`
sin90lzc
  • 浏览: 157225 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Hibernate Search(基于version3.4)--第四章Mapping entities to the index structure

阅读更多

Mapping entities to the index structure

 

4.1. 映射一个实体(Mapping an entity)

在第一章中,你已经知道了建立实体索引的所有元信息是通过注解描述的,所以不需要xml的映射文件。但是你依然可以使用Hibernate的映射文件来配置基本的Hibernate映射,但Hibernate Search的配置只能通过注解来表达。

 

4.1.1.基本映射(Basic mapping)

我们先介绍最常使用的注解。

 

4.1.1.1. @Indexed

首先,我们必须要声明一个持久化类是可索引的。这可以由注解@Indexed来注明,所有没有@Indexed的实体将忽略indexing步骤。

 

Example 4.1. Making a class indexable with @Indexed

@Entity
@Indexed
public class Essay {
    ...
}

你也可以设置@Indexed的index属性来指定index的名称。默认是使用持久化类的全限定名。For more information see Section 3.2, “Directory configuration”.

 

4.1.1.2. @Field

对于每个持久化类的属性域,你都可以声明它们怎样去建立索引。如果属性域上没有注解描述,那么该属性域将忽略indexing。@Field声明一个属性域可以被索引,@Field的属性可以用于配置具体indexing的过程。

  • name:保存在Lucene中Field的名字。默认为属性名。
  • store:属性域的值是否要保存到Lucene Index中。Store.YES,花费更大的硬盘空间但允许projection(see Section 5.1.3.5, “Projection”);Store.COMPRESS,会消耗更多的CPU资源; Store.NO,不保存属性域的值,这是默认值。
  • index:定义属性域怎样被建立索引和哪些类型的信息会被保存。Index.NO,不会被建立索引,所以该域不能被搜索;Index.TOKENIZED,使用解析器(ananlyzer)去解析该属性域;Index.UN_TOKENIZED,不需要经过解析器解析,整个属性域的值作为索引值;Index.NO_NORMS,不保存normalization数据(其实是Lucene中域的Boost值)。默认值是TOKENIZED。

Tip :解析一个日期型属性域是没必要的。

Tip :用作排序的域是不能被解析的。

  • termVector:如何描述term-frequency对。这个属性指定是否保存documents的term vectors。默认值是TermVector.NO。

 

termVector的可选值如下表所示:

Value Definition
TermVector.YES 保存每个文档的term vector。这种方式会产生两个同步的数组,一个数组包含了document的terms,另一个数组包含对应term的频率
TermVector.NO 不何在term vector。
TermVector.WITH_OFFSETS 不仅保存term vector,还保存term的offset信息(包含term的开始和结束的位置信息)。
TermVector.WITH_POSITIONS 不仅保存term vector,还保存term的position信息(包含term在document中出现的位置信息)
TermVector.WITH_POSITION_OFFSETS TermVector.WITH_POSITIONS和TermVector.WITH_OFFSETS的组合。

 

  • indexNullAs:默认情况下,null值是会被忽略,不会被索引的。然而,使用indexNullAs可以指定一个字符串代替null值。indexNullAs默认值是Field.DO_NOT_INDEX_NULL,那么null值并不会被索引。也可以修改这个值为Field.DEFAULT_NULL_TOKEN去指定一个字符串代替null token。这个字符串通过属性hibernate.search.default_null_token配置。但是如果使用了indexNullAs=Field.DEFAULT_NULL_TOKEN而又没有配置hibernate.search.default_null_token属性,字符串"_null_"会用来代替null token。

Note :当使用了indexNullAs属性,你必须使用相同的token去查询null值。当然,要使用该功能最好是与un-tokenized fields一齐使用。

 

Warning :如果实现了FieldBridge 或 TwoWayFieldBridge,indexing和null值的维护的责任就只能交给开发者了。(see JavaDocs of
LuceneOptions.indexNullAs())

 

4.1.1.3. @NumericField

@NumericField必须要与@Field一起使用。该注解的使用范围与@Field,@DocumentId是一样的。它用于标注Integer, Long, Float和Double类型的属性域。在index time,该值会用Trie结构[http://en.wikipedia.org/wiki/Trie]来创建索引。当一个属性域用numeric field来创建索引,那么它就能使用高效的范围查询(range query)和排序(sorting)了。@NumericField注解接受下表所示的属性:

Value Definition
forField 可选的。指定对应的@Field的名称。只有在属性域上有超过一个@Field声明时,才会强制要求添加该属性。
precisionStep 可选的。改变Trie结构的精度。值越小,消耗更多的硬盘空间,但能提高范围查询和排序。值越大,则相反。默认值为4

 

 

4.1.1.4. @Id

最后,一个实体的id属性域是一个特别的属性,Hibernate Search用这个属性来确保index与实体的对应关系。默认情况下,id属性应该被保存和不能被tokenized。可以使用注解@DocumentId来标志一个属性为index id。如果某个属性域上有@Id注解,也可以省略@DocumentId。

 

Example 4.2. Specifying indexed properties

@Entity
@Indexed
public class Essay {
    ...
    @Id
    @DocumentId
    public Long getId() { return id; }
    
    @Field(name="Abstract", index=Index.TOKENIZED, store=Store.YES)
    public String getSummary() { return summary; }
    
    @Lob
    @Field(index=Index.TOKENIZED)
    public String getText() { return text; }
    
    @Field 
    @NumericField( precisionStep = 6)
    public float getGrade() { return grade; }
}

 

Example 4.2, “Specifying indexed properties” 定义了一个index包含了4个field:id , Abstract,text和grade。grade被注解为Numeric和一个比默认值稍微大点precisionStep。

 

 

4.1.2. 多次映射同一个属性(Mapping properties multiple times)

有时候可能需要映射同一个属性多次,这种方式使用了一个有点不同的indexing策略。假设有下面这样的情景:某个人想要查询某个属性域是否包含某个token并要按该属性域排序时,那么他必须index这个属性域两次,因为查询属性域,属性域必须TOKENIZED,而对属性域排序又必须UN_TOKENIZED。通过@Fields注解可以达到这个目的。

 

Example 4.3. Using @Fields to map a property multiple times

@Entity
@Indexed(index = "Book" )
public class Book {
    @Fields( {
            @Field(index = Index.TOKENIZED),
            @Field(name = "summary_forSort", index = Index.UN_TOKENIZED, store = Store.YES)
            } )
    public String getSummary() {
        return summary;
    }
    ...
}

 

Example 4.3, “Using @Fields to map a property multiple times”中,summary属性域被索引了两次,一次是以summary作为索引文档的域名,以tokenized的方式建立索引,另一次是以summary_forSort作为索引文档的域名,以untokenized的方式建立索引。在@Fields中,@Field支持两个有用的属性:

  • analyzer:定义@Analyzer注解针对具体的lucene field而不是实体内中的属性域。
  • bridge:定义@FieldBridge注解针对具体的lucene field而不是实体内中的属性域。

关于@Analyzer和@FieldBridge,下面的章节有更详细的说明。

 

 

4.1.3. 嵌入和关联对象(Embedded and associated objects)

关联对象和嵌入的对象都可以被索引作为根实体索引的一部分。当实体内中的属性域为关联对象时,这就会显得非常的有用。

 

假设在Example 4.4, “Indexing associations”例子中,它的目的是想要返回city为Atlanta的place,那么在Lucene query parser language中,它会被解析为address.city:Atlanta。例子中会建立名为Place的index,该index会包含这些field: address.id, address.street和address.city,这些field都可以用于搜索。

 

Example 4.4. Indexing associations

@Entity
@Indexed
public class Place {
    @Id
    @GeneratedValue
    @DocumentId
    private Long id;
    
    @Field( index = Index.TOKENIZED )
    private String name;
    
    @OneToOne( cascade = { CascadeType.PERSIST, CascadeType.REMOVE } )
    @IndexedEmbedded
    private Address address;
    ....
}
@Entity
public class Address {
    @Id
    @GeneratedValue
    private Long id;
    
    @Field(index=Index.TOKENIZED)
    private String street;
    
    @Field(index=Index.TOKENIZED)
    private String city;
    
    @ContainedIn
    @OneToMany(mappedBy="address")
    private Set<Place> places;
    ...
}

 

需要注意的是,当使用@IndexedEmbedded技术嵌入对象时,Lucene的index数据会变得不正常。因为Hibernate Search需要有能力知道Place对象和Address对象的改变来使index保持最新状态。为了确保当Address改变后,Place的Lucene document也会随之更新,需要在双向关联的另一边添加注解@ContainedIn(在本例中是在Address.places属性域上添加注解)。

 

Tip :@ContainedIn is only useful on associations pointing to entities as opposed to embedded (collection of) objects.

 

让我们看一个更复杂的例子 Example 4.5, “Nested usage of @IndexedEmbedded and @ContainedIn”.

 

Example 4.5. Nested usage of @IndexedEmbedded and @ContainedIn

@Entity
@Indexed
public class Place {
    @Id
    @GeneratedValue
    @DocumentId
    private Long id;
    
    @Field( index = Index.TOKENIZED )
    private String name;
    
    @OneToOne( cascade = { CascadeType.PERSIST, CascadeType.REMOVE } )
    @IndexedEmbedded
    private Address address;
    ....
}

@Entity
public class Address {
    @Id
    @GeneratedValue
    private Long id;
    
    @Field(index=Index.TOKENIZED)
    private String street;
    
    @Field(index=Index.TOKENIZED)
    private String city;
    
    @IndexedEmbedded(depth = 1, prefix = "ownedBy_")
    private Owner ownedBy;
    
    @ContainedIn
    @OneToMany(mappedBy="address")
    private Set<Place> places;
    ...
}

@Embeddable
public class Owner {
    @Field(index = Index.TOKENIZED)
    private String name;
   ...
}

 

正如你所看到的一样,所有的标注了@*ToMany, @*ToOne 和 @Embedded 的属性域都可以注解为@IndexedEmbedded,相关联对象的属性域就会被添加到根实体的index中去。Example 4.5, “Nested usage of @IndexedEmbedded and @ContainedIn”生成的index将包含以下的field:id,name,address.street,address.city,address.ownedBy_name。根据传统的object navigation convention,关联对象的属性field name使用默认的前缀propertyName. 。也可以在 @IndexedEmbedded标注中添加prefix属性来自定义前缀(如例子中的ownedBy属性)。

 

当对象是一个循环类依赖的结构的时候(例如Owner存在一个引用指向Place),就应该指定depth 属性。那么只有当嵌入的属性到达指定的depth时(或达到了对象图界限时),Hibernate Search才会停止indexing。在例子Example 4.5中,因为depth为1,因此在Owner类中标注了@IndexedEmbedded的属性域都会被忽略。

 

使用@IndexedEmbedded标识对象关联,也是

 

 

使用Lucene查询语法也能表达@IndexedEmbedded标注的对象关联关系:

  • 返回place名字中包含有JBoss 并且address的city为Atlanta
    +name:jboss +address.city:atlanta
     
  •  返回place名字中包含有JBoss 而且owner的名字包含有Joe
    +name:jboss +address.orderBy_name:joe
     

Note :一个被关联的对象自身也可以是@Indexed,但并不是必需的。

 

当@IndexedEmbedded标注了一个实体,那么关联就必须是双向的,并且另一端的引用必须标注为@ContainedIn(如前面的例子)。如果不这样的话,当关联对象改变的时候,Hibernate Search将不会跟着更新根实体的index。

 

有时候,标注了@IndexedEmbedded的对象的类型并不是具体的对象类型(例如接口和它们的实现类)。这时候,就需要使用targetElement属性来指定具体的对象类型。

 

Example 4.6. Using the targetElement property of @IndexedEmbedded

@Entity
@Indexed
public class Address {
    @Id
    @GeneratedValue
    @DocumentId
    private Long id;
    
    @Field(index= Index.TOKENIZED)
    private String street;
    
    @IndexedEmbedded(depth = 1, prefix = "ownedBy_", targetElement = Owner.class)
    @Target(Owner.class)
    private Person ownedBy;
    
    ...
}

@Embeddable
public class Owner implements Person { ... }

 

 

 

4.2. 比重(Boosting)

Lucene中有一个概念叫做Boosting。Boosting用于定义某些document或field相对于其他document或field的比重。Lucene在index time的boosting与search time的boosting是不一样的。下面的章节介绍如何在Hibernate Search中完成index time的boosting。

 

4.2.1. Static index time boosting

可以通过类级别或属性级别的注解@Boost定义一个static boost值。先可以通过@Field.boost来指定。

 

Example 4.7. Different ways of using @Boost

@Entity
@Indexed
@Boost(1.7f)
public class Essay {
    ...
  
    @Id
    @DocumentId
    public Long getId() { return id; }
    
    @Field(name="Abstract", index=Index.TOKENIZED, store=Store.YES, boost=@Boost(2f))
    @Boost(1.5f)
    public String getSummary() { return summary; }
    
    @Lob
    @Field(index=Index.TOKENIZED, boost=@Boost(1.2f))
    public String getText() { return text; }
    
    @Field
    public String getISBN() { return isbn; }
}

 

 

4.2.2. Dynamic index time boosting

在有些情况下,boost因子可能依赖于实际对象的状态。在这种情况下,你就要自定义一个BoostStrategy的实现,并使用@DynamicBoost来注册该实现。

 

Example 4.8. Dynamic boost examle

public enum PersonType {
    NORMAL,
    VIP
}

@Entity
@Indexed
@DynamicBoost(impl = VIPBoostStrategy.class)
public class Person {
    private PersonType type;   
    
    // ....
}

public class VIPBoostStrategy implements BoostStrategy {
    public float defineBoost(Object value) {
        Person person = ( Person ) value;
        if ( person.getType().equals( PersonType.VIP ) ) {
            return 2.0f;
        }
        else {
            return 1.0f;
        }
    }
}

 

在Example 4.8, “Dynamic boost examle”中,dynamic boost定义在类级别,指定了BoostStrategy的实现类VIPBoostStrategy。@DynamicBoost可以标注在类级别或属性级别上,这依赖于你想要传递整个对象还是属性域的值给BoostStrategy的defineBoost 方法。在上例中,VIP比normal用户的比重大2倍。

 

Note :BoostStrategy的实现必须定义一个无参构造函数。

 

 

4.3. 解析(Analysis)

Analysis是一个把文本转换成terms(索引词,搜索词)的过程,是全文搜索引擎中非常重要的一个功能。Lucene使用Analyzer来管理这个过程。下面的章节将介绍如何在Hibernate Search中配置Analyzer。

 

4.3.1. Default analyzer and analyzer by class

默认的analyzer 通过hibernate.search.analyzer属性配置。该属性的默认值为org.apache.lucene.analysis.standard.StandardAnalyzer。

 

你也可以在实体类、属性域、甚至在@Field(对于单属性域多个field的情形来说很有用)中配置Analyzer。

 

Example 4.9. Different ways of using @Analyzer

@Entity
@Indexed
@Analyzer(impl = EntityAnalyzer.class)
public class MyEntity {
    @Id
    @GeneratedValue
    @DocumentId
  private Integer id;
    
    @Field(index = Index.TOKENIZED)
    private String name;
    
    @Field(index = Index.TOKENIZED)
    @Analyzer(impl = PropertyAnalyzer.class)
    private String summary;
    
    @Field(index = Index.TOKENIZED, analyzer = @Analyzer(impl = FieldAnalyzer.class)
    private String body;


    ...
}

 

在这个例子里面,MyEntity属性域的解析工作由EntityAnalyzer完成,除了summary 和 body属性域,它们分别由PropertyAnalyzer,FieldAnalyzer来解析。

 

Caution :在同一个实体中混合多个不同的analyzer并不是一个好的实践。它会使得建立查询变得更复杂和使得结果很难预料(对于新手来说),特别是当你使用一个QueryParser来创建Query时,QueryParser在整个查询过程中需要使用相同的analyzer。根据经验所得,不管是indexing time还是search time,所有的field都应该使用相同的analyzer。

 

 

4.3.2. analyzer的命名(Named analyzers)

Analyzer可以变得非常复杂。基于这个原因,HibernateSearch中提出一个概念analyzer definition。analyzer definition可以由@Analyzer注解重用。analyzer definition由以下几部分组成:

 

  • a name:analyzer definition的唯一的引用名。
  • a list of char filters:每个char filter在划分分词(tokenization)之前对输入源(字符集)进行预处理。Char filter可以添加、修改、除出字符。最常用的一个用途是字符的规范化。
  • a tokenizer:负责把输入源划分成一个一个的词语。
  • a list of filters:负责添加,删除,修改由tokenizer解析出来的词语。

Analyzer的工作被划分成一系列的任务,这些任务由一系列的char filter,一个tokenizer和一系列的filter来完成,每个char filter,filter,tokenizer都能完成一项具体的任务,把char filter,filter,tokenizer组合起来就是一个analyzer了(像玩积木一样)。简而言之,char filters完成输入字符的预处理,Tokenizer把文本流划分成一个一个的词语,再经由TokenFilter对生成的词语进行过滤。Hibernate Search利用了Solr analyzer framework来提供这方面的支持。

 

Note :有些analyzer和filter需要额外的依赖包。例如使用 snowball stemmer解析器,需要添加 lucene-snowball的jar包,对于 PhoneticFilterFactory,还需要 commons-codec [ http://commons.apache.org/codec ] 的jar包 。Hibernate Search提代了这些jar包在它的distribution的lib/optional目录下。查看Table 4.2,“Example of available tokenizers” 和 Table 4.3, “Examples of available filters”找到更多analyzer或filter所需的依赖包。在之前的Hibernate Search版本3.3.0.Beta2,当用到 analyzer definition framework时, 需要添加Solr的依赖 org.apache.solr:solr-core。当然,如果你使用Maven来管理依赖,这就没必要了,因为所有的Solr依赖被定义在 artifact为 org.hibernate:hibernate-search-analyzers中。只需要pom中添加下面的配置

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-search-analyzers</artifactId>
   <version>3.4.0.Final</version>
<dependency>

 

让我们看一个具体的例子——Example 4.10, “@AnalyzerDef and the Solr framework”。首先一个char filter通过它的factory来定义。在这个例子中,使用的是一个mapping char filter,它会基于指定的mapping file来替代输入流中的字符。之后是tokenizer的定义。这个例子使用标准的tokenizer。最后,一系列的filter通过它们的factory来定义。在这个例子中,StopFilter过滤器会读取在property file中指定的专有名词。该过滤器将忽略大小写。

 

 

Example 4.10. @AnalyzerDef and the Solr framework

@AnalyzerDef(name="customanalyzer",
  charFilters = {
    @CharFilterDef(factory = MappingCharFilterFactory.class, params = {
      @Parameter(name = "mapping",
        value = "org/hibernate/search/test/analyzer/solr/mapping-chars.properties")
    })
  },
  tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
  filters = {
    @TokenFilterDef(factory = ISOLatin1AccentFilterFactory.class),
    @TokenFilterDef(factory = LowerCaseFilterFactory.class),
    @TokenFilterDef(factory = StopFilterFactory.class, params = {
      @Parameter(name="words",
        value= "org/hibernate/search/test/analyzer/solr/stoplist.properties" ),
      @Parameter(name="ignoreCase", value="true")
    })
  })
public class Team {
    ...
}

 

 

Tip :Filters 和 char filters会按他们在@AnalyzerDef中定义的顺序来应用的。因些顺序是重要的。

 

有些tokenizers, token filters 或char filters需要加载资源文件(比如配置文件,元数据文件等)。stop filter和synonym filter就需要这样的文件。如果资源文件的charset与VM默认的不一样,你可以通过resource_charset 参数明确地指定资源文件的charset。

 

Example 4.11. Use a specific charset to load the property file

 

@AnalyzerDef(name="customanalyzer",
  charFilters = {
    @CharFilterDef(factory = MappingCharFilterFactory.class, params = {
      @Parameter(name = "mapping",
        value = "org/hibernate/search/test/analyzer/solr/mapping-chars.properties")
    })
  },
  tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
  filters = {
    @TokenFilterDef(factory = ISOLatin1AccentFilterFactory.class),
    @TokenFilterDef(factory = LowerCaseFilterFactory.class),
    @TokenFilterDef(factory = StopFilterFactory.class, params = {
      @Parameter(name="words",
        value= "org/hibernate/search/test/analyzer/solr/stoplist.properties" ),
      @Parameter(name="resource_charset", value = "UTF-16BE"),
      @Parameter(name="ignoreCase", value="true")
  })
})
public class Team {
    ...
}

 

 

 

一旦定义了一个Analyzer,它就可以通过@Analyzer注解来重用。如Example 4.12, “Referencing an analyzer by name”.所示。

 

Example 4.12. Referencing an analyzer by name

@Entity
@Indexed
@AnalyzerDef(name="customanalyzer", ... )
public class Team {
    @Id
    @DocumentId
    @GeneratedValue
    private Integer id;
    
    @Field
    private String name;
    
    @Field
    private String location;
    
    @Field 
    @Analyzer(definition = "customanalyzer")
    private String description;
}

 

由@AnalyzerDef声明的Analyzer同样可以用在SearchFactory中,这在创建query时会非常有用。

 

Analyzer analyzer = fullTextSession.getSearchFactory().getAnalyzer("customanalyzer");

 

查询字符串应该使用indexing time相同的analyzer,看作两者在说同一种“语言”:在query和indexing的过程中都生成同样的tokens。这个规则也会产生一些问题,但对于大多数时候是很正确的。请遵循这个规则,除非你知道如何处理使用不同analyzer的情形。

 

 

4.3.2.1. 可用的解析器(Available analyzers)

Solr和Lucene自带了许多有用的char filter,tokenizer,filter。你可以在 http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters 找到完整的char filter factories, tokenizer factories 和 filter factories列表。让我们看一下其中的一部分解析器。

Table 4.1. Example of available char filters

 

Factory Description Parameters Additional
dependencies
MappingCharFilterFactory

按提供的mapping file替代某些字符

mapping:指向一个资源文件包含如下的mapping格式

                    "á" => "a"
                    "ñ" => "n"
                    "ø" => "o"

 none
HTMLStripCharFilterFactory  移除HTML中标准的标签,保留文本内容  none  none

 

 

Table 4.2. Example of available tokenizers

Factory Description Parameters Additional
dependencies
StandardTokenizerFactory 使用Lucene的StandardTokenizer none none
HTMLStripCharFilterFactory 移除HTML中标准的标签,保留文本容,之后交由StandardTokenizer处理 none solr-core
PatternTokenizerFactory

通过一个指定的正则表达式来划分词语。

pattern:用于划分词语的正则表达式。

group:says which pattern group to extract into tokens

solr-core

 

Table 4.3. Examples of available filters

 

Factory Description Parameters Additional
dependencies
StandardFilterFactory 从token中移除'.'和''s' none solr-core
LowerCaseFilterFactory 小写所有的token none solr-core
StopFilterFactory 移除与列表中的stop word匹配的token

words:指向一个包含stop words的资源文件

ignoreCase:与stop word的比较是否要忽略大小写

solr-core
SnowballPorterFilterFactory

把一个单词还原为它的词干形式(像protect, protects,
protection都会使用protect作为token)

language:Danish,Dutch, English,Finnish, French,German, Italian,Norwegian,Portuguese, Russian,Spanish, Swedish 或其他语言 solr-core
ISOLatin1AccentFilterFactory

移除发音符,比如French(法语)

none solr-core
PhoneticFilterFactory

向token stream插入发音相似的单词作为相似token(similar token)

encoder:值为DoubleMetaphone,Metaphone, Soundex或RefinedSoundex之一。

 

inject:true就插入新的token到token stream,false就替代现有的token。

 

maxCodeLength:设置可生成code的最大长度。只支持Metaphone和DoubleMetaphone encoder。

solr-core and commons-codec
CollationKeyFilterFactory

转换每个token成java.text.CollationKey,并使用IndexableBinaryStringTools对java.text.CollationKey进行编码,从而把它保存到index term。

 

 custom,  language,country,  variant,strength,decomposition  see Lucene's CollationKeyFilter javadocs for more info  solr-core and commons-io

 

我们推荐通过IDE工具查看org.apache.solr.analysis.TokenizerFactory和org.apache.solr.analysis.TokenFilterFactory的所有可用的实现。

 

4.3.3. 动态解析器选择(试验性的)(Dynamic analyzer selection (experimental))

到目前为止,我们所定义的analyzer都是静态的。然而,可能有这样的需求,我们需要根据实体类的具体状态来决定使用哪个analyzer,比如说在一个多语言的应用环境中。拿Example 4.13, “Usage of @AnalyzerDiscriminator”中的BlogEntry类来说,analyzer需要根据language属性来决定使用哪个analyzer,那么对词干(stemmer)解析就可以指定正确的语言来解析文本。

 

Hibernate Search引进了AnalyzerDiscriminator注解来实现Dynamic analyzer selection。Example 4.13, “Usage of @AnalyzerDiscriminator”展示了这个注解的使用。

 

Example 4.13. Usage of @AnalyzerDiscriminator

@Entity
@Indexed
@AnalyzerDefs({
  @AnalyzerDef(name = "en",
    tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
    filters = {
      @TokenFilterDef(factory = LowerCaseFilterFactory.class),
      @TokenFilterDef(factory = EnglishPorterFilterFactory.class
      )
    }),
  @AnalyzerDef(name = "de",
    tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
    filters = {
      @TokenFilterDef(factory = LowerCaseFilterFactory.class),
      @TokenFilterDef(factory = GermanStemFilterFactory.class)
    })
})
public class BlogEntry {
    @Id
    @GeneratedValue
    @DocumentId
    private Integer id;
    
    @Field
    @AnalyzerDiscriminator(impl = LanguageDiscriminator.class)
    private String language;
    
    @Field
    private String text;
    
    private Set<BlogEntry> references;
    // standard getter/setter
    ...
}

public class LanguageDiscriminator implements Discriminator {
     public String getAnalyzerDefinitionName(Object value, Object entity, String field) {
        if ( value == null || !( entity instanceof Article ) ) {
            return null;
        }
        return (String) value;
    }
}

 

使用@AnalyzerDiscriminator的先决条件是所有的analyzers都通过@AnalyzerDef来预定义。@AnalyzerDiscriminator可应用于类级别和属性级别。通过impl参数来指定Discriminator接口具体的实现类。该接口只需要实现getAnalyzerDefinitionName() 方法,它是在field添加到Lucene document的时候调用。那个被索引的实体也会被传递给接口方法。value参数只有当@AnalyzerDiscriminator应用于属性域上时才会被传递进来。在这个例子中,language属性的当前值传递进了接口方法。

 

所有的Discriminator接口的实现类都应该返回已定义analyzer的名称,如果返回的是null,则使用默认的analyzer。

 

Note :@AnalyzerDiscriminator依然处于测试阶段,这里的API可能会在以后的版本中修改。我们希望得到你们的使用反馈给我们社区。

 

4.3.4. 获取Analyzer(Retrieving an analyzer)

在某些情形下,需要手动控制来获取Analyzer。例如,在域模型中使用了多个analyzer,在生成query的你又需要使用与index time相同analyzer。

 

Note :如果你有一个好的理由的话,你可以打破这个规则。如果你不确定的话,应该使用相同的analyzer。如果你使用的是Hibernate Search query DSL (see Section 5.1.2,“Building a Lucene query with the Hibernate Search query DSL”),你不需要关心这一点。因为 query DSL会完全透明地使用正确的analyzer。

 

无论你使用的是Lucene编程API还是Lucene的QueryParser,你都可以获取某个实体类的scoped analyzer。scoped analyzer是一个能自动地对索引中的field使用正确的analyzer的解析器。记住这一点,在一个实体类中,可以为各个属性域定义不同的analyzer。这个原理看起来有点复杂,但在query中使用正确的analyzer是一件很容易的事。

 

Example 4.14. Using the scoped analyzer when building a full-text query

org.apache.lucene.queryParser.QueryParser parser = new QueryParser(
    "title", 
    fullTextSession.getSearchFactory().getAnalyzer( Song.class )
);
org.apache.lucene.search.Query luceneQuery = 
    parser.parse( "title:sky Or title_stemmed:diamond" );
org.hibernate.Query fullTextQuery = 
    fullTextSession.createFullTextQuery( luceneQuery, Song.class );
List result = fullTextQuery.list(); //return a list of managed objects 

 

在这个例子中,song title被索引在两个field中,title field使用的是standard analyzer,而title_stemmed使用的是stemming analyzer。query会根据对应的field使用了合适的analyzer。

 

Tip :通过@AnalyzerDef定义的analyzer,你同样通过searchFactory.getAnalyzer(String)来获取。

 

4.4. Bridges

在我们讨论实体类的基本映射的时候,一个很重要的因素一直被我们忽略。在Lucene所有的index field必须渲染为字符串类型。所有标注为@Field的属性域都要转换为String来建立索引。我们到现在才提出这个问题是因为在大多数情况下,Hibnerate Search都会自动地完成转换工作,这些工作都由内建的bridge完成的。然而,有些时候你需要一个更好的控制这个转换过程。

 

4.4.1. Built-in bridges

Hibernate Search捆绑了大量的bridge完成Java属性类型到字符串的转换。

 

 

 

Hibernate Search comes bundled with a set of built-in bridges between a Java property type and
its full text representation.

 

null:默认地,null值不会被索引。Lucene也不支持null元素。然而有时候使用一个自定义的token来代替null值会显得非常有用。 See Section 4.1.1.2, “@Field” for more information

 

java.lang.String:按原本的值来索引。

 

short, Short, integer, Integer, long, Long, float, Float, double, Double, BigInteger, BigDecimal的数值型类型:转换到字符串的表示形式。注意,Lucene不能自动完成数值型的对比工作(如在range query中),它们需要补齐(padded)。

 

Note :使用range query是有争议的而且有一些缺点。一个可选的方法是使用一个Filter query,它可以过滤一个查询到合适的范围。Hibernate Search支持补齐机制(padding mechanism)。

 

java.util.Date:日期型保存成yyyyMMddHHmmssSSS的字符串形式 (例如200611072203012表示2006年11月7日 22:03 12ms) 。你不必为它的格式化忧心,重要的是,当你使用DateRange查询的时候,你需要知道日期应该表示成GMT形式。

 

一般来说,保存的Date不需要精确到毫秒。@DateBridge定义了合适的解决方法让你去保存Date到index。@DateBridge(resolution=Resolution.DAY)定义了Date只保存到具体的'日',而不是毫秒。

 

@Entity 
@Indexed
public class Meeting {
    @Field(index=Index.UN_TOKENIZED)
    @DateBridge(resolution=Resolution.MINUTE)
    private Date date;
    ...    
}

 

Warning :当Date的resolution低于MILLISECOND,不能标注为@DocumentId

 

java.lang.Class:转换到它的全限定名。The thread context classloader is used when the class is rehydrated

 

 

4.4.2. 自定义bridge(Custom bridges)

有时候,当Hibernate Search内建的bridge并不能满足你的需要的时候,那么你就需要自定义bridge。下面的段落介绍了一些解决方法。

 

4.4.2.1. StringBridge

这是最简单的解决方法建立Object到String的bridge,你只需要向Hibernate Search提交org.hibernate.search.bridge.StringBridge接口的实现类。所有的实现类都必须是线程安全的。

 

Example 4.15. Custom StringBridge implementation

/**
 * Padding Integer bridge.
 * All numbers will be padded with 0 to match 5 digits
 *
 * @author Emmanuel Bernard
 */
public class PaddedIntegerBridge implements StringBridge {
    
    private int PADDING = 5;
    
    public String objectToString(Object object) {
        String rawInteger = ( (Integer) object ).toString();
        if (rawInteger.length() > PADDING) 
            throw new IllegalArgumentException( "Try to pad on a number too big" );
        StringBuilder paddedInteger = new StringBuilder( );
        for ( int padIndex = rawInteger.length() ; padIndex < PADDING ; padIndex++ ) {
            paddedInteger.append('0');
        }
        return paddedInteger.append( rawInteger ).toString();
    }
}

 

你可以使用通过@FieldBridge注解的impl参数应用上面定义的StringBridge。

@FieldBridge(impl = PaddedIntegerBridge.class)
private Integer length;  

 

4.4.2.1.1. 参数化bridge(Parameterized bridge)

向bridge的实现类传递参数会变得更加灵活。Example 4.16, “Passing parameters to your bridge implementation”  实现了ParameterizedBridge接口并参数通过注解@FieldBridge传递进来。

 

Example 4.16. Passing parameters to your bridge implementation

public class PaddedIntegerBridge implements StringBridge, ParameterizedBridge {
    
    public static String PADDING_PROPERTY = "padding";
    private int padding = 5; //default
    
    public void setParameterValues(Map parameters) {
        Object padding = parameters.get( PADDING_PROPERTY );
        if (padding != null) this.padding = (Integer) padding;
    }
    
    public String objectToString(Object object) {
        String rawInteger = ( (Integer) object ).toString();
        if (rawInteger.length() > padding) 
            throw new IllegalArgumentException( "Try to pad on a number too big" );
        StringBuilder paddedInteger = new StringBuilder( );
        for ( int padIndex = rawInteger.length() ; padIndex < padding ; padIndex++ ) {
            paddedInteger.append('0');
        }
        return paddedInteger.append( rawInteger ).toString();
    }
}


//property
@FieldBridge(impl = PaddedIntegerBridge.class,
             params = @Parameter(name="padding", value="10")
            )
private Integer length;

 

ParameterizedBridge接口可以与StringBridge,TwoWayStringBridge,FieldBridge一起实现。所有的实现必须是线程安全的,参数必须在初始化时设置。

 

4.4.2.1.2. 按类型的bridge(Type aware bridge)

 

有时候能知道bridge应用在哪个类型上面是非常有用的:

  • 知道getter方法返回类型的bridge
  • 知道类的类型通过类级别的bridge

一个例子是bridge处理enums在一种自定义的形式,但在访问时应用实际的enum类型。任何实现了 AppliedOnTypeAwareBridge接口的bridge会自动的注入所需要的类型。像parameters一样,类型注入不需要关心线程安全。

 

 

4.4.2.1.3. 两种方式的bridge(Two-way bridge)

如果你的bridge实现应用在id属性(使用了@DocumentId注解),你需要使用TwoWayStringBridge接口,该接口继承于StringBridge接口。Hibernate Search需要读取id的字符串表示并生成对象表示形式。同样也是使用@FieldBridge注解。

 

Example 4.17. Implementing a TwoWayStringBridge usable for id properties

public class PaddedIntegerBridge implements TwoWayStringBridge, ParameterizedBridge {
    public static String PADDING_PROPERTY = "padding";
    private int padding = 5; //default
    public void setParameterValues(Map parameters) {
        Object padding = parameters.get( PADDING_PROPERTY );
        if (padding != null) this.padding = (Integer) padding;
    }
    public String objectToString(Object object) {
        String rawInteger = ( (Integer) object ).toString();
        if (rawInteger.length() > padding) 
            throw new IllegalArgumentException( "Try to pad on a number too big" );
        StringBuilder paddedInteger = new StringBuilder( );
        for ( int padIndex = rawInteger.length() ; padIndex < padding ; padIndex++ ) {
            paddedInteger.append('0');
        }
        return paddedInteger.append( rawInteger ).toString();
    }
    public Object stringToObject(String stringValue) {
        return new Integer(stringValue);
    }
}
//id property
@DocumentId
@FieldBridge(impl = PaddedIntegerBridge.class,
             params = @Parameter(name="padding", value="10") 
private Integer id;

 

 

 

4.4.2.2. FieldBridge

有些用例不仅仅是把对象转换成字符串来映射属性到lucene index中,但实现FieldBridge会让你得到最大的灵活性。该接口为你提供了属性值,并让你按你所希望的方式映射到Lucene Document。比如说你希望把一个属性域保存在两个不同的document field中。这个接口的概念与Hibernate UserType非常类似。

 

Example 4.18. Implementing the FieldBridge interface

/**
 * Store the date in 3 different fields - year, month, day - to ease Range Query per
 * year, month or day (eg get all the elements of December for the last 5 years).
 * @author Emmanuel Bernard
 */
public class DateSplitBridge implements FieldBridge {
    private final static TimeZone GMT = TimeZone.getTimeZone("GMT");
    
    public void set(String name, Object value, Document document, 
                    LuceneOptions luceneOptions) {
        Date date = (Date) value;
        Calendar cal = GregorianCalendar.getInstance(GMT);
        cal.setTime(date);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH) + 1;
        int day = cal.get(Calendar.DAY_OF_MONTH);
  
        // set year
        luceneOptions.addFieldToDocument(
            name + ".year",
            String.valueOf( year ),
            document );
  
        // set month and pad it if needed
        luceneOptions.addFieldToDocument(
            name + ".month",
            month < 10 ? "0" : "" + String.valueOf( month ),
            document );
  
        // set day and pad it if needed
        luceneOptions.addFieldToDocument(
            name + ".day",
            day < 10 ? "0" : "" + String.valueOf( day ),
            document );
    }
}

//property
@FieldBridge(impl = DateSplitBridge.class)
private Date date;

 

在Example 4.18, “Implementing the FieldBridge interface”中,fields并不是直接地添加到Document。相反,添加操作委托给LuceneOptions helper;helper会应用注解中的@Field的选项,像Store,TermVector或@Boost值。这也用于封装复杂的COMPRESS的实现。虽然委托LuceneOptions去添加fields到Document是推荐的,但你也可以直接编辑Document和忽略LuceneOptions。

 

Tip :像LuceneOptions这些类从Lucene API中变化而来的,它们作用是保护你的应用和简化代码。你可以使用它们,但并不是必须的。

 

4.4.2.3. ClassBridge

有时候组合一个实体的多个属性到Lucene index中会很有用的。@ClassBridge包含一个或多个@ClassBridge可以定义在class级别。在这种情况下, 自定义的field bridge的value参数不再是属性域的值,而是该实体实例。@ClassBridge支持termVector属性。

 

Example 4.19. Implementing a class bridge

@Entity
@Indexed
@ClassBridge(name="branchnetwork",
             index=Index.TOKENIZED,
             store=Store.YES,
             impl = CatFieldsClassBridge.class,
             params = @Parameter( name="sepChar", value=" " ) )
public class Department {
    private int id;
    private String network;
    private String branchHead;
    private String branch;
    private Integer maxEmployees
    ...
}
public class CatFieldsClassBridge implements FieldBridge, ParameterizedBridge {
    private String sepChar;
    public void setParameterValues(Map parameters) {
        this.sepChar = (String) parameters.get( "sepChar" );
    }

    public void set(
        String name, Object value, Document document, LuceneOptions luceneOptions) {
        // In this particular class the name of the new field was passed
        // from the name field of the ClassBridge Annotation. This is not
        // a requirement. It just works that way in this instance. The
        // actual name could be supplied by hard coding it below.
        Department dep = (Department) value;
        String fieldValue1 = dep.getBranch();
        if ( fieldValue1 == null ) {
            fieldValue1 = "";
        }
        String fieldValue2 = dep.getNetwork();
        if ( fieldValue2 == null ) {
            fieldValue2 = "";
        }
        String fieldValue = fieldValue1 + sepChar + fieldValue2;
        Field field = new Field( name, fieldValue, luceneOptions.getStore(),
            luceneOptions.getIndex(), luceneOptions.getTermVector() );
        field.setBoost( luceneOptions.getBoost() );
        document.add( field );
   }
}

 

在这个例子中, CatFieldsClassBridge被应用到 department实例,field bridge结合了branch和network属性域并添加到index。

 

4.5. Providing your own id

Warning :这部分文档的工作还在进行中。

 

如果你扩展了Hibernate Search内部功能,你可以提供你自己的id。你可以生成唯一的id值添加到index中去。它必须在创建an org.hibernate.search.Work的时候提供给Hibernate Search——document id是构造器中的一个参数。

 

4.5.1. The ProvidedId annotation

@PrivideId与@DocumentId不一样,它是标注在类级别上的。你可以通过bridge属性来指定一个自定义的bridge实现。同样地,如果你使用@ProvidedId注解一个类,该类的子类也会也会得到该注解,但是它不是通过@Inherited得到这种效果的。另外,要确保@ProvidedId与@DocumentId不能同时使用,否则你的系统就会崩溃。

 

Example 4.20. Providing your own id

@ProvidedId (bridge = org.my.own.package.MyCustomBridge)
@Indexed
public class MyClass{
    @Field
    String MyString;
    ...
}

 

4.6. Programmatic API 

Warning:该功能还在测试阶段。API在以后可能会有所改变。

 

略。 

 

 

 

 

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics