1. 程式人生 > >Java for Web學習筆記(一二三):搜尋(5)MySQL全文索引(下)

Java for Web學習筆記(一二三):搜尋(5)MySQL全文索引(下)

小例子

我們在表格Ticket和TicketComment中加入了fulltext key。小例子在Ticket的Subject或Body,以及在TicketComment的Body檢索內容,按分頁方式顯示出來,同時顯示關聯分數,並按關聯分數降序排列。

 -- Ticket中隊Subject和Body這兩列進行全文檢索
 FULLTEXT KEY `Ticket_Search` (`Subject`,`Body`),
 -- TicketComment中對Body列進行全文檢索。
 FULLTEXT KEY `TicketComment_Search` (`Body`),

增加一個搜尋結果,涵蓋TicketEntity和關聯分數

根據需求,我們將檢索兩個表格的內容,獲取分數,以檢索hello為例子,SQL語句如下
-- 顯示Ticket表的內容
SELECT DISTINCT t.*,
   -- 顯示關聯分數(將兩個表格的關聯分數加起來),列名為 ft_scoreColumn
   (MATCH(t.Subject, t.Body) AGAINST("hello") + MATCH(c.Body) AGAINST("hello")) AS _ft_scoreColumn 
      -- 將表格Ticket和TicketComment 根據ticket.id join在一起
      FROM Ticket t LEFT OUTER JOIN TicketComment c ON c.TicketId = t.TicketId 
         -- where存在關聯性
         WHERE MATCH(t.Subject, t.Body) AGAINST("hello") OR  MATCH(c.Body) AGAINST("hello")
            -- 設定排序
            ORDER BY _ft_scoreColumn DESC, TicketId DESC;

在關聯表格的返回中,通常會包含多個內容,並不是只與某個Entity相對應,在本例中就含有分值,我們將學習如何對映這樣的結果。

建立存放結果的類SearchResult

public class SearchResult<T> {
	private final T entity;
	private final double relevance;
	public SearchResult(T entity, double relevance) {
		this.entity = entity;
		this.relevance = relevance;
	}
	public T getEntity() {
		return entity;
	}
	public double getRelevance() {
		return relevance;
	}	
}

@SqlResultSetMapping提供返回結果的對映關係

我們將這個對映關係命名為"searchResultMapping.ticket",放在TicketEntity中,當然也可以放在其他的Class,只要標記@SqlResultSetMapping即可。

 @Entity
 @Table(name = "Ticket")
 @SqlResultSetMapping(
     name = "searchResultMapping.ticket",
     entities = { @EntityResult(entityClass = TicketEntity.class) },
     columns = { @ColumnResult(name = "_ft_scoreColumn", type = Double.class)}
 )
 public class TicketEntity implements Serializable

這種方式等同與xml的配置。

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" 
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
                                     http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
                 version="2.1">
    <sql-result-set-mapping name="searchResultMapping.ticket">
        <entity-result entity-class="com.wrox.site.entities.TicketEntity" />
        <column-result name="_ft_scoreColumn" class="java.lang.Double" />
    </sql-result-set-mapping>
</entity-mappings>

這個配置一般位於/META-INF/orm.xml。也可以在persistence.xml中通過<mapping-file>來執行位置,或者通過下面的程式碼來執行位置。

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(){
    ...
    factory.setJpaPropertyMap(properties);
    factory.setMappingResources("com/example/config/mappings.xml");
    return factory;
}

在createNativeQuery中將結果進行對映

我們使用了原生的SQL語句,對映方式如下,例子的具體實現在後面介紹

Query query = entityManager.createNativeQuery(sql, "searchResultMapping.ticket");
List<Object[]> results = query.getResultList();
for(Object[] result : results){
    //result[0]對應searchResultMapping.ticket的第一項entites裡面的TicketEntity.class
    //result[1]對應searchResultMapping.ticket的第二項,因為entities只有一項,因此為columns中的name = "_ft_scoreColumn", type = Double.class
}

在倉庫中增加查詢介面

public interface SearchableRepository<T>{
    Page<SearchResult<T>> search(String query, boolean useBooleanMode, Pageable pageable);
}
public interface TicketRepository extends CrudRepository<TicketEntity, Long>,SearchableRepository<TicketEntity>{
}

介面實現

public class TicketRepositoryImpl implements SearchableRepository<TicketEntity>{
	@PersistenceContext EntityManager entityManager;
	
	@Override
	public Page<SearchResult<TicketEntity>> search(String query, boolean useBooleanMode, Pageable pageable) {
		String mode = useBooleanMode ?	"IN BOOLEAN MODE" : "IN NATURAL LANGUAGE MODE";
		String matchTicket = "MATCH(t.Subject, t.Body) AGAINST(?1 " + mode + ")";
		String matchComment = "MATCH(c.Body) AGAINST(?1 " + mode + ")";
		
		//1】分頁需要獲得總數以及該頁的資料,顯示獲取總數。請參考前面對sql的說明。
		String sql = "SELECT COUNT(DISTINCT t.TicketId) FROM Ticket t " +
						"LEFT OUTER JOIN TicketComment c ON c.TicketId = " +
						"t.TicketId WHERE " + matchTicket + " OR " + matchComment;
		//對於原生SQL的方式,返回結果是BigInteger不能直接轉換為Long。採用了Number來進行。
		long total = ((Number) this.entityManager.createNativeQuery(sql).setParameter(1, query).getSingleResult())
				.longValue();
		
		//2】獲取該頁的資訊,
		sql = "SELECT DISTINCT t.*, (" + matchTicket + " + " + matchComment +") AS _ft_scoreColumn " + 
				"FROM Ticket t LEFT OUTER JOIN TicketComment c ON c.TicketId = t.TicketId " +
				"WHERE " + matchTicket + " OR " + matchComment + " " +
				"ORDER BY _ft_scoreColumn DESC, TicketId DESC";
		@SuppressWarnings("unchecked")
		List<Object[]> results = this.entityManager.createNativeQuery(sql, "searchResultMapping.ticket")
			.setParameter(1, query)
				.setFirstResult(pageable.getOffset())
				.setMaxResults(pageable.getPageSize())
					.getResultList();

		//3】將結果轉為我們定義SearchResult。
		List<SearchResult<TicketEntity>> list = new ArrayList<>();
		results.forEach(o -> list.add(
		                        new SearchResult<TicketEntity>((TicketEntity)o[0], (Double)o[1])));
		
		return new PageImpl<>(list,pageable,total);
	}
}
使用createNativeQuery而不是criteria JPA介面意味著實現和底層和資料庫相關,如果更換為其他資料庫,需要重新編寫程式碼,而有些資料庫支援fulltext key有些不支援。