如何實現分佈式搜索(一)實現簡單的分詞器

一、搜索功能分析

搜索可以說是咱們生活中非常常見的一個功能了,基本上只要是個互聯網公司都離不開搜索模塊,但是實現它的方案卻有點麻煩。

衆所周知,咱們的數據都是從數據庫來的,因此一講到搜索,我們就會想能不能用數據庫解決,然後只要是個正常人,大腦經過0.01秒的思考,就會拋棄數據庫~

原因有三:

1、數據庫的搜索(也就是模糊查詢)不智能。

我想要搜索 ‘北京的大學’ 出來的結果絕對不會包含‘北京大學’,更別說搜索出北大了。

2、數據庫的搜索太慢

咱們搜索一個數據,基本上就是全表掃描,對於數據庫來說,花個一天兩天也可能是正常的,基本上喫個飯睡個覺,也許你的搜索頁面就有結果了呢?~

當然可能會有人說建立索引。但是啊,萬事萬物都是互通的,所謂的數據庫索引不過也要基於你的底層代碼來實現,對於字符串的索引,是一個詞一個詞建立的,因此,當模糊查詢搜索字符串的時候,他能快速的從第一個字符搜索,也就是滿足‘like  字符串%’,而對於'like %字符串%'的sql,它可不會走索引。

3、 數據庫要是被搜索打崩了,獻祭哪個程序員祭天好呢?

 

二、針對數據庫查詢的缺點提出解決方案

估計很多人會直接想到使用solr或者elasticsearch,lucene等等第三方工具來解決這些問題,沒錯,這些都是主流技術手段,但不是我們剖析的重點。我想授人以魚不如授人以漁,明白了原理,學習別人的第三方工具也會非常簡單。

廢話少說,針對上述的三個問題,我們可以分析出如下的結果:

1、搜索模塊業務量挺大,需要單獨抽取出一個模塊,這叫做服務解耦。

2、爲了加快搜索速度,最好直接將要被搜索的數據內容存在內存而非硬盤,並且提供持久化方案,宕機後也能讀取本地數據立馬啓動。

3、對於搜索,我們需要對搜索詞進行分詞和過濾,然後根據分詞的結果,去搜索對應的數據。

 

三、實現分析

上面的解決方案已經初步提出了搜索模塊的一個雛形,現在我們需要考慮的就是如何實現這三個需求。

需求1比較抽象,但是實現起來並不難。

需求2內容太大,但是目前看來實現起來就是類似數據庫的數據導入一樣,並不難(如果不包含建立索引等等的話~

需求3則是這一節我要講的內容:實現分詞和過濾。

 

 

四、分詞和過濾具體實現

(一)凡人的解決方案

其實很多人對於分詞和過濾都是有個實現方案的,那也是我第一次接觸搜索的時候想到的方案。

既然需要實現分詞,那麼首先我們得告訴我們的程序,哪些字符串組合在一起是關鍵詞,哪些字符串組合又是過濾詞。

因此我們可以使用最簡單的實現方式======》利用兩個HashSet來存儲關鍵詞和過濾詞。使用HashSet的原因也很簡單,自帶去重。

之後只需要拿搜索詞去和這個hashSet一一比較,就可以得出這個搜索詞中包含了哪些關鍵詞和過濾詞。

代碼實現如下:

順便吐槽一句切回eclipse敲代碼真的不順手,還是idea好用。。

public class ParticipleBuilder {
	//分詞器關鍵詞
	private Set<String> keyWords = new HashSet<String>();
	//分詞器關鍵詞最大長度和最小長度
	private int keyWordMaxLength = 3;
	private int keyWordMinLength = 2;
	
	public ParticipleBuilder(){
		keyWords.add("北京");
		keyWords.add("大學");
		keyWords.add("劇院");
		keyWords.add("京劇");
		keyWords.add("天安門");
	}
	
	
	public List<String> executor(String search){
		List containWords = new LinkedList<String>();
		
		for(int index = 0;index<search.length();index++){
			//根據分詞器關鍵詞最大長度和最小長度,獲取一個詞然後判斷他是不是敏感詞
			for(int subLength = keyWordMinLength;subLength <= keyWordMaxLength;subLength++){
				//防止下標越界
				if(index+subLength>search.length())break;
				//截取關鍵詞並且判斷是否是關鍵詞
				String word = search.substring(index, index+subLength);
				System.out.println(word+"是關鍵詞嗎?");
				if(keyWords.contains(word)){
					//返回true則是關鍵詞,添加,跳過該詞繼續向下掃描
					System.out.println("是");
					containWords.add(word);
					index += subLength-1;
					break;
					
				}else{
					System.out.println("不是");
					continue;
				}
			}
			
		}
		containWords.add(search);
		return containWords;
	}
	
	public static void main(String[] args) {
		System.out.println(new ParticipleBuilder().executor("我想去北京劇院,天安門廣場"));
		
	}
	

}

 

(二) 有b格的解決方案

hashSet雖然可以幫我們解決這個問題,但是他存在的問題也很明顯,用它來分詞基本上就是字符串做比較,缺點很多,最大的缺點就是慢,其次,大量的重複字符也會佔用內存。雖然我們利用了HashSet的Hash算法來優化字符串匹配的速度,能夠快速的判斷一個字符串是不是關鍵詞,但是字符串在分割的時候浪費了大量的時間和內存,因此,他並不是最好的解決方案。

那麼,對於一個詞的查找,有什麼辦法呢?

其實是有的,小學的時候買過的厚厚的中文典給了我們解決方案,衆所周知,詞典查詞的時候,就比如  博主很帥  ,我們先找到博字所在的頁碼,接着找到主zhu所在的頁碼,然後一個字一個字的找,就會發現,這個詞,壓根找不到。

好吧,扯遠了,但是博主真的很帥。

既然現在有了解決思路,我們就要想辦法用代碼來實現。現在,中文詞典就是我們的關鍵詞集合。‘博主很帥’則是我們搜索的詞,我們要做的事情,就是通過中文詞典,找出博主很帥中有幾個關鍵詞。

那麼問題來了,我們要怎麼樣,才能讓關鍵詞集合像中文詞典一樣,開頭一樣的詞都在一起呢?

這裏又要給大家介紹一個數據結構了,叫做trie樹。

參考:輕鬆看懂trie樹

看懂trie樹之後接下來就是代碼實現:

(該代碼需要對數據結構有一定掌握,不能手寫hashMap的人請跳過。)

從trie樹的樹狀圖我們可以用HashMap來實現,也可以自定義數據結構來實現,此處爲了方便,用了hashMap。使用HashMap的缺點就是會造成大量的空間浪費,好處就是時間複雜度最小,最快。

其次就是需要注意,如何標誌這個詞已經結尾。

如果使用自定義數據類型,這個是很簡單的,定義一個bool變量即可,但是我們使用HashMap,存儲類型是<string,map>就不能這樣做了,我使用的方案是,對於詞中國,給‘’這個key對應的value值,也就是一個map,添加一個符號‘/$’做key,(絕大部分情況下,這些符號不會參加分詞,也毫無意義)。

        // 構建trie樹
	private Map<String, Map> trieKeyWords = new HashMap<String, Map>();

	// 先從本地讀取一個關鍵詞集合
	private String[] keywordsArray = { "中國", "中國銀行", "銀行", "京劇", "北京天安門", "北京" };

初始化你的樹

// 開始構建一個trie樹
	for (String word : keywordsArray) {
		// 獲取trie根節點指針
		Map<String, Map> trieMap = trieKeyWords;
		System.out.println("判斷" + word);
		for (int l = 0; l < word.length(); l++) {
			String str = word.substring(l, l + 1);
			// 先判斷當前字符是否已經存在
			if (trieMap.get(str) == null) {
				System.out.println(str + "不存在,添加該節點");
				// 不存在,則添加,並創建下一顆子樹,將指針移到這個新節點
				Map newTrieMap = new HashMap(4);
				trieMap.put(str, newTrieMap);
				trieMap = newTrieMap;
			} else {
				System.out.println(str + "存在,移動到兒子節點");
				// 存在,移動指針節點去下一個map,啥也不幹。
				trieMap = trieMap.get(str);
			}
			// 對於最後一個字,需要做額外判斷,標識他是一個詞
			if (l + 1 == word.length()) {
				// 結束,標誌該詞結束了。
				System.out.println(word + "已經成功添加");
				trieMap.put("/$", new HashMap(0));
			}
		}
	}
	System.out.println(trieKeyWords);

開始分詞

// 分詞
	public List<String> executor(String search) {
		// 保存分詞結果
		List<String> result = new LinkedList();

		for (int index = 0; index < search.length(); index++) {
			// 獲取trie根節點指針
			Map<String, Map> trieMap = trieKeyWords;
			// 保存關鍵詞
			String keywords = "";
			for (int i = index; i < search.length(); i++) {
				// 判斷該詞是否到末尾
				if (trieMap.get("/$") != null) {
					// 這是一個詞,將該詞添加到list
					result.add(keywords);
					System.out.println(keywords + "是一個關鍵詞");
				}
				// 判斷是否有後續
				String key = search.substring(i, i + 1);
				if (trieMap.get(key) != null) {
					// 有後續則繼續向下
					System.out.println(key+"有後續,繼續");
					keywords += key;

				} else {
					// 沒有後續就可以拜拜了
					break;
				}
				// 指針移動到下一位
				trieMap = trieMap.get(key);
				//如果到末尾了再判斷一次,防止最後一個詞漏掉
				if (i+1==search.length()&&trieMap.get("/$") != null) {
					// 這是一個詞,將該詞添加到list
					result.add(keywords);
					System.out.println(keywords + "是一個關鍵詞");
				}
				
			}
		}

		return result;
	}

測試與結果

 

現在我們已經實現了對關鍵詞的分詞,這樣,我們搜索的時候,就可以根據分詞結果來搜索,也不會對搜索條件太過苛刻,更不會因爲數據庫裏沒有‘螺旋炮彈自走式風衣’這樣的商品而什麼也搜索不出了,哈哈哈。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章