1. 程式人生 > >用Python實現一個大數據搜索及源代碼

用Python實現一個大數據搜索及源代碼

Python編程語言 Python案例講解 Python基礎精講

在日常生活中,大家了解搜索引擎如百度、360、搜狗、谷歌等,搜索是大數據領域裏常見的需求。Splunk和ELK分別是該領域在非開源和開源領域裏的領導者。本文利用很少的Python代碼實現了一個基本的數據搜索功能,試圖讓大家理解大數據搜索的基本原理。

  布隆過濾器(BloomFilter)

  第一步我們先要實現一個布隆過濾器。

  布隆過濾器是大數據領域的一個常見算法,它的目的是過濾掉那些不是目標的元素。也就是說如果一個要搜索的詞並不存在與我的數據中,那麽它可以以很快的速度返回目標不存在。

  讓我們看看以下布隆過濾器的代碼:

  classBloomfilter(object):

  """

  ABloomfilterisaprobabilisticdata-structurethattradesspaceforaccuracy

  whendeterminingifavalueisinaset.Itcantellyouifavaluewaspossibly

  added,orifitwasdefinitelynotadded,butitcan‘ttellyouforcertainthat

  itwasadded.

  """

  definit(self,size):

  """SetuptheBFwiththeappropriatesize"""

  self.values=[False]*size

  self.size=size

  defhash_value(self,value):

  """HashthevalueprovidedandscaleittofittheBFsize"""

  returnhash(value)%self.size

  defadd_value(self,value):

  """AddavaluetotheBF"""

  h=self.hash_value(value)

  self.values[h]=True

  defmight_contain(self,value):

  """CheckifthevaluemightbeintheBF"""

  h=self.hash_value(value)

  returnself.values[h]

  defprint_contents(self):

  """DumpthecontentsoftheBFfordebuggingpurposes"""

  printself.values

  基本的數據結構是個數組(實際上是個位圖,用1/0來記錄數據是否存在),初始化是沒有任何內容,所以全部置False。實際的使用當中,該數組的長度是非常大的,以保證效率。

  利用哈希算法來決定數據應該存在哪一位,也就是數組的索引

  當一個數據被加入到布隆過濾器的時候,計算它的哈希值然後把相應的位置為True

  當檢查一個數據是否已經存在或者說被索引過的時候,只要檢查對應的哈希值所在的位的True/Fasle

  看到這裏,大家應該可以看出,如果布隆過濾器返回False,那麽數據一定是沒有索引過的,然而如果返回True,那也不能說數據一定就已經被索引過。在搜索過程中使用布隆過濾器可以使得很多沒有命中的搜索提前返回來提高效率。

  我們看看這段code是如何運行的:

  bf=Bloomfilter(10)

  bf.add_value(‘dog‘)

  bf.add_value(‘fish‘)

  bf.add_value(‘cat‘)

  bf.print_contents

  bf.add_value(‘bird‘)

  bf.print_contents

  #Note:contentsareunchangedafteraddingbird-itcollides

  fortermin[‘dog‘,‘fish‘,‘cat‘,‘bird‘,‘duck‘,‘emu‘]:

  print‘{}:{}{}‘.format(term,bf.hash_value(term),bf.might_contain(term))

  結果:

  [False,False,False,False,True,True,False,False,False,True]

  [False,False,False,False,True,True,False,False,False,True]

  dog:5True

  fish:4True

  cat:9True

  bird:9True

  duck:5True

  emu:8False

  首先創建了一個容量為10的的布隆過濾器

  然後分別加入‘dog’,‘fish’,‘cat’三個對象,這時的布隆過濾器的內容如下:

  然後加入‘bird’對象,布隆過濾器的內容並沒有改變,因為‘bird’和‘fish’恰好擁有相同的哈希。

  最後我們檢查一堆對象(’dog’,‘fish’,‘cat’,‘bird’,‘duck’,’emu’)是不是已經被索引了。結果發現‘duck’返回True,2而‘emu’返回False。因為‘duck’的哈希恰好和‘dog’是一樣的。

  分詞

  下面一步我們要實現分詞。分詞的目的是要把我們的文本數據分割成可搜索的最小單元,也就是詞。這裏我們主要針對英語,因為中文的分詞涉及到自然語言處理,比較復雜,而英文基本只要用標點符號就好了。

  下面我們看看分詞的代碼:

  defmajor_segments(s):

  """

  Performmajorsegmentingonastring.Splitthestringbyallofthemajor

  breaks,andreturnthesetofeverythingfound.Thebreaksinthisimplementation

  aresinglecharacters,butinSplunkpropertheycanbemultiplecharacters.

  Asetisusedbecauseorderingdoesn‘tmatter,andduplicatesarebad.

  """

  major_breaks=‘‘

  last=-1

  results=set

  #enumeratewillgiveus(0,s[0]),(1,s[1]),...

  foridx,chinenumerate(s):

  ifchinmajor_breaks:

  segment=s[last+1:idx]

  results.add(segment)

  last=idx

  #Thelastcharactermaynotbeabreaksoalwayscapture

  #thelastsegment(whichmayendupbeing"",butyolo)

  segment=s[last+1:]

  results.add(segment)

  returnresults

  主要分割

  主要分割使用空格來分詞,實際的分詞邏輯中,還會有其它的分隔符。例如Splunk的缺省分割符包括以下這些,用戶也可以定義自己的分割符。

  ]<>{}|!;,‘”*\n\r\s\t&?+%21%26%2526%3B%7C%20%2B%3D—%2520%5D%5B%3A%0A%2C%28%29

  defminor_segments(s):

  """

  Performminorsegmentingonastring.Thisislikemajor

  segmenting,exceptitalsocapturesfromthestartofthe

  inputtoeachbreak.

  """

  minorbreaks=‘.‘

  last=-1

  results=set

  foridx,chinenumerate(s):

  ifchinminor_breaks:

  segment=s[last+1:idx]

  results.add(segment)

  segment=s[:idx]

  results.add(segment)

  last=idx

  segment=s[last+1:]

  results.add(segment)

  results.add(s)

  returnresults

  次要分割

  次要分割和主要分割的邏輯類似,只是還會把從開始部分到當前分割的結果加入。例如“1.2.3.4”的次要分割會有1,2,3,4,1.2,1.2.3

  defsegments(event):

  """Simplewrapperaroundmajor_segments/minor_segments"""

  results=set

  formajorinmajor_segments(event):

  forminorinminor_segments(major):

  results.add(minor)

  returnresults

  分詞的邏輯就是對文本先進行主要分割,對每一個主要分割在進行次要分割。然後把所有分出來的詞返回。

  我們看看這段code是如何運行的:

  forterminsegments(‘src_ip=1.2.3.4‘):

  printterm

  src

  1.2

  1.2.3.4

  src_ip

  3

  1

  1.2.3

  ip

  2

  =

  4

  搜索

  好了,有個分詞和布隆過濾器這兩個利器的支撐後,我們就可以來實現搜索的功能了。上代碼:

  classSplunk(object):

  definit(self):

  self.bf=Bloomfilter(64)

  self.terms={}#Dictionaryoftermtosetofevents

  self.events=

  defadd_event(self,event):

  """Addsaneventtothisobject"""

  #GenerateauniqueIDfortheevent,andsaveit

  event_id=len(self.events)

  self.events.append(event)

  #Addeachtermtothebloomfilter,andtracktheeventbyeachterm

  forterminsegments(event):

  self.bf.add_value(term)

  iftermnotinself.terms:

  self.terms[term]=set

  self.terms[term].add(event_id)

  defsearch(self,term):

  """Searchforasingleterm,andyieldalltheeventsthatcontainit"""

  #InSplunkthisrunsinO(1),andislikelytobeinfilesystemcache(memory)

  ifnotself.bf.might_contain(term):

  return

  #InSplunkthisprobablyrunsinO(logN)whereNisthenumberoftermsinthetsidx

  iftermnotinself.terms:

  return

  forevent_idinsorted(self.terms[term]):

  yieldself.events[event_id]

  Splunk代表一個擁有搜索功能的索引集合

  每一個集合中包含一個布隆過濾器,一個倒排詞表(字典),和一個存儲所有事件的數組

  當一個事件被加入到索引的時候,會做以下的邏輯

  為每一個事件生成一個unqieid,這裏就是序號

  對事件進行分詞,把每一個詞加入到倒排詞表,也就是每一個詞對應的事件的id的映射結構,註意,一個詞可能對應多個事件,所以倒排表的的值是一個Set。倒排表是絕大部分搜索引擎的核心功能。

  當一個詞被搜索的時候,會做以下的邏輯

  檢查布隆過濾器,如果為假,直接返回

  檢查詞表,如果被搜索單詞不在詞表中,直接返回

  在倒排表中找到所有對應的事件id,然後返回事件的內容

  我們運行下看看把:

  s=Splunk

  s.add_event(‘src_ip=1.2.3.4‘)

  s.add_event(‘src_ip=5.6.7.8‘)

  s.add_event(‘dst_ip=1.2.3.4‘)

  foreventins.search(‘1.2.3.4‘):

  printevent

  print‘-‘

  foreventins.search(‘src_ip‘):

  printevent

  print‘-‘

  foreventins.search(‘ip‘):

  printevent

  src_ip=1.2.3.4

  dst_ip=1.2.3.4

  -

  src_ip=1.2.3.4

  src_ip=5.6.7.8

  -

  src_ip=1.2.3.4

  src_ip=5.6.7.8

  dst_ip=1.2.3.4

  是不是很贊!

  更復雜的搜索

  更進一步,在搜索過程中,我們想用And和Or來實現更復雜的搜索邏輯。

  上代碼:

  classSplunkM(object):

  definit(self):

  self.bf=Bloomfilter(64)

  self.terms={}#Dictionaryoftermtosetofevents

  self.events=

  defadd_event(self,event):

  """Addsaneventtothisobject"""

  #GenerateauniqueIDfortheevent,andsaveit

  event_id=len(self.events)

  self.events.append(event)

  #Addeachtermtothebloomfilter,andtracktheeventbyeachterm

  forterminsegments(event):

  self.bf.add_value(term)

  iftermnotinself.terms:

  self.terms[term]=set

  self.terms[term].add(event_id)

  defsearch_all(self,terms):

  """SearchforanANDofallterms"""

  #Startwiththeuniverseofallevents...

  results=set(range(len(self.events)))

  forterminterms:

  #Ifatermisn‘tpresentatallthenwecanstoplooking

  ifnotself.bf.might_contain(term):

  return

  iftermnotinself.terms:

  return

  #Dropeventsthatdon‘tmatchfromourresults

  results=results.intersection(self.terms[term])

  forevent_idinsorted(results):

  yieldself.events[event_id]

  defsearch_any(self,terms):

  """SearchforanORofallterms"""

  results=set

  forterminterms:

  #Ifatermisn‘tpresent,weskipit,butdon‘tstop

  ifnotself.bf.might_contain(term):

  continue

  iftermnotinself.terms:

  continue

  #Addtheseeventstoourresults

  results=results.union(self.terms[term])

  forevent_idinsorted(results):

  yieldself.events[event_id]

  利用Python集合的intersection和union操作,可以很方便的支持And(求交集)和Or(求合集)的操作。

  運行結果如下:

  s=SplunkM

  s.add_event(‘src_ip=1.2.3.4‘)

  s.add_event(‘src_ip=5.6.7.8‘)

  s.add_event(‘dst_ip=1.2.3.4‘)

  foreventins.search_all([‘src_ip‘,‘5.6‘]):

  printevent

  print‘-‘

  foreventins.search_any([‘src_ip‘,‘dst_ip‘]):

  printevent

  src_ip=5.6.7.8

  -

  src_ip=1.2.3.4

  src_ip=5.6.7.8

  dst_ip=1.2.3.4

用Python實現一個大數據搜索及源代碼