拿起筆來做刀槍 · 之五 再造一個lucene

實關於全文檢索的倒排序,邏輯是非常簡單的,“空間換時間”的概念也不復雜。

我們先做一個最簡單的分詞器,使用“二元切分”,亦即“二元” “元切” “切分”。

package net.csdn.blog.deltatang.lucene4me;

import java.util.HashMap;
import java.util.Map;

public class Analyzer {

	public Map<String, Integer> analyze(String text){
		
		Map<String, Integer> words = new HashMap<String, Integer>();		
		for(int i = 0; i < text.length() - 1; i++) {
			String word = text.substring(i, i + 1);
			int freq = 1;
			if(words.keySet().contains(word)) {
				freq += words.get(word);
			}
			words.put(word, freq);
		}		
		return words;
	}
	
}
這樣,我們記錄了一個文檔內容的所有切分詞(token)以及其對應的詞頻(在文檔中出現的次數)

當然,在查詢中,查詢詞也是需要切分詞的,這時候需要注意的是,

編制索引所用的分詞器和查詢詞的分詞器必須是同一個實現,否則,就會驢脣不對馬嘴:)


然後,簡便起見,我們用一個hash表來作爲內存索引存儲。

package net.csdn.blog.deltatang.lucene4me;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class DataStore {
	
	private static Map<String, Set<Doc>> dataset = new HashMap<String, Set<Doc>>();
	
	public static void insert(String token, int docID, int freq) {
		Set<Doc> docIDs = dataset.get(token);
		if(docIDs == null) {
			docIDs = new HashSet<Doc>();
			dataset.put(token, docIDs);
		}
		docIDs.add(new Doc(docID, freq));
	}
	
	public static Set<Doc> get(String token) {
		return dataset.get(token);				
	}

        public static void display() {
                System.out.println(dataset);
        }
 }
這個存儲裏面,存儲了 token 和 文檔一對多的關係。

雖然實現比較粗糙,但是,所謂“倒排序”的含義就在於此。

我們前面提到的爲什麼要前後端都用同一分詞器的原因也在於此。試想,如果前端對查詢詞采取不同的分詞器,那就不能確保會生成 存儲表裏面 對應的token。

也就無法檢索到需要的結果。

這個,其實就是lucene最關鍵的部分——簡單就是美——沒錯兒:)


接下來,我們打造一個工具類,用於支持創建索引和查詢索引,

創建索引的方法如下:

	public static void buildIndex(int id, String text) {
		Map<String, Integer> datas = analyzer.analyze(text);
		
		for(String token : datas.keySet()) {
			DataStore.insert(token, id, datas.get(token));
		}		
	}

爲了便於測試和說明,我們新增一個初始化方法,添加一部分模擬數據:

	static {
		init();
	}
	
	private static void init() {
		buildIndex(1, "解放區的天是明朗的天");
		buildIndex(2, "從來就沒有什麼救世主,也沒有神仙皇帝");
		buildIndex(3, "中國人民從此站立起來了");
		buildIndex(4, "東風吹,戰鼓擂,如今世上誰怕誰");
		buildIndex(5, "不是東風壓倒西風,就是西風壓倒東風");
		buildIndex(6, "人民萬歲,人民萬歲");
	}

然後我們做一個查詢方法,並且實現查詢結果根據詞頻排序:

	public static int[] findDocIDs(String keywords) {
		
		TreeSet<Doc> results = new TreeSet<Doc>(new Comparator<Doc>() {

			@Override
			public int compare(Doc o1, Doc o2) {
				int val = o2.freq - o1.freq; 
				return val == 0 ? 1 : val;
			}
		});
		
		Map<String, Integer> datas = analyzer.analyze(keywords);
		for(String token : datas.keySet()) {
			Set<Doc> docs = DataStore.get(token);
			if(docs != null && !docs.isEmpty()) {
				results.addAll(docs);	
			}				
		}				
		
		int[] ret = new int[results.size()];
		Iterator<Doc> itor = results.iterator();
		
		for(int i = 0; i < ret.length && itor.hasNext(); i++) {
			ret[i] = itor.next().id;
		}
		
		return ret;
	}

OK,最後,我們測試一下:

	public static void main(String[] args) {
		
		DataStore.display();
		
		int[] ret;
		ret = findDocIDs("東風");
		for(int r : ret) {
			System.out.print(r + ", ");
		}
		System.out.println();

		ret = findDocIDs("人民");
		for(int r : ret) {
			System.out.print(r + ", ");
		}
		System.out.println();
		
		ret = findDocIDs("東風人民");
		for(int r : ret) {
			System.out.print(r + ", ");
		}
		System.out.println();
	}

輸出的結果爲:

{從來=[<2, 1>], 民從=[<3, 1>], 放區=[<1, 1>], 有什=[<2, 1>], 就是=[<5, 1>], 是東=[<5, 1>], 世上=[<4, 1>], 區的=[<1, 1>], 的天=[<1, 2>], 擂,=[<4, 1>], 來了=[<3, 1>], 中國=[<3, 1>], 人民=[<3, 1>, <6, 2>], 國人=[<3, 1>], 今世=[<4, 1>], 明朗=[<1, 1>], 西風=[<5, 2>], 如今=[<4, 1>], ,戰=[<4, 1>], 萬歲=[<6, 2>], 歲,=[<6, 1>], 沒有=[<2, 2>], 救世=[<2, 1>], 上誰=[<4, 1>], 風壓=[<5, 2>], 是明=[<1, 1>], 戰鼓=[<4, 1>], 是西=[<5, 1>], 吹,=[<4, 1>], 也沒=[<2, 1>], 民萬=[<6, 2>], 倒西=[<5, 1>], 從此=[<3, 1>], 什麼=[<2, 1>], 仙皇=[<2, 1>], 壓倒=[<5, 2>], 朗的=[<1, 1>], ,如=[<4, 1>], ,也=[<2, 1>], 神仙=[<2, 1>], 誰怕=[<4, 1>], 風,=[<5, 1>], 天是=[<1, 1>], 起來=[<3, 1>], 世主=[<2, 1>], ,就=[<5, 1>], 麼救=[<2, 1>], ,人=[<6, 1>], 倒東=[<5, 1>], 皇帝=[<2, 1>], 有神=[<2, 1>], 此站=[<3, 1>], 東風=[<4, 1>, <5, 2>], 就沒=[<2, 1>], 解放=[<1, 1>], 立起=[<3, 1>], 主,=[<2, 1>], 站立=[<3, 1>], 風吹=[<4, 1>], 鼓擂=[<4, 1>], 怕誰=[<4, 1>], 不是=[<5, 1>], 來就=[<2, 1>]}
5, 4, 
6, 3, 
6, 5, 3, 4, 

可以看到,結果正確,排序正確。

bingo!我們繼續前進了一步!


LuceneTool 的完整代碼如下:

package net.csdn.blog.deltatang.lucene4me;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

public class LuceneTool {
	
	private static Analyzer analyzer = new Analyzer();
	
	static {
		init();
	}
	
	private static void init() {
		buildIndex(1, "解放區的天是明朗的天");
		buildIndex(2, "從來就沒有什麼救世主,也沒有神仙皇帝");
		buildIndex(3, "中國人民從此站立起來了");
		buildIndex(4, "東風吹,戰鼓擂,如今世上誰怕誰");
		buildIndex(5, "不是東風壓倒西風,就是西風壓倒東風");
		buildIndex(6, "人民萬歲,人民萬歲");
	}
	
	public static void buildIndex(int id, String text) {
		Map<String, Integer> datas = analyzer.analyze(text);
		
		for(String token : datas.keySet()) {
			DataStore.insert(token, id, datas.get(token));
		}		
	}
	
	public static int[] findDocIDs(String keywords) {
		
		TreeSet<Doc> results = new TreeSet<Doc>(new Comparator<Doc>() {

			@Override
			public int compare(Doc o1, Doc o2) {
				int val = o2.freq - o1.freq; 
				return val == 0 ? 1 : val;
			}
		});
		
		Map<String, Integer> datas = analyzer.analyze(keywords);
		for(String token : datas.keySet()) {
			Set<Doc> docs = DataStore.get(token);
			if(docs != null && !docs.isEmpty()) {
				results.addAll(docs);	
			}				
		}				
		
		int[] ret = new int[results.size()];
		Iterator<Doc> itor = results.iterator();
		
		for(int i = 0; i < ret.length && itor.hasNext(); i++) {
			ret[i] = itor.next().id;
		}
		
		return ret;
	}


}


發佈了94 篇原創文章 · 獲贊 6 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章