sphinxapi.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318
  1. #
  2. # $Id$
  3. #
  4. # Python version of Sphinx searchd client (Python API)
  5. #
  6. # Copyright (c) 2006, Mike Osadnik
  7. # Copyright (c) 2006-2016, Andrew Aksyonoff
  8. # Copyright (c) 2008-2016, Sphinx Technologies Inc
  9. # All rights reserved
  10. #
  11. # This program is free software; you can redistribute it and/or modify
  12. # it under the terms of the GNU Library General Public License. You should
  13. # have received a copy of the LGPL license along with this program; if you
  14. # did not, you can find it at http://www.gnu.org/
  15. #
  16. # WARNING!!!
  17. #
  18. # As of 2015, we strongly recommend to use either SphinxQL or REST APIs
  19. # rather than the native SphinxAPI.
  20. #
  21. # While both the native SphinxAPI protocol and the existing APIs will
  22. # continue to exist, and perhaps should not even break (too much), exposing
  23. # all the new features via multiple different native API implementations
  24. # is too much of a support complication for us.
  25. #
  26. # That said, you're welcome to overtake the maintenance of any given
  27. # official API, and remove this warning ;)
  28. #
  29. from __future__ import print_function
  30. import sys
  31. import select
  32. import socket
  33. import re
  34. from struct import *
  35. if sys.version_info > (3,):
  36. long = int
  37. text_type = str
  38. else:
  39. text_type = unicode
  40. # known searchd commands
  41. SEARCHD_COMMAND_SEARCH = 0
  42. SEARCHD_COMMAND_EXCERPT = 1
  43. SEARCHD_COMMAND_UPDATE = 2
  44. SEARCHD_COMMAND_KEYWORDS = 3
  45. SEARCHD_COMMAND_PERSIST = 4
  46. SEARCHD_COMMAND_STATUS = 5
  47. SEARCHD_COMMAND_FLUSHATTRS = 7
  48. # current client-side command implementation versions
  49. VER_COMMAND_SEARCH = 0x120
  50. VER_COMMAND_EXCERPT = 0x104
  51. VER_COMMAND_UPDATE = 0x103
  52. VER_COMMAND_KEYWORDS = 0x100
  53. VER_COMMAND_STATUS = 0x101
  54. VER_COMMAND_FLUSHATTRS = 0x100
  55. # known searchd status codes
  56. SEARCHD_OK = 0
  57. SEARCHD_ERROR = 1
  58. SEARCHD_RETRY = 2
  59. SEARCHD_WARNING = 3
  60. # known match modes
  61. SPH_MATCH_ALL = 0
  62. SPH_MATCH_ANY = 1
  63. SPH_MATCH_PHRASE = 2
  64. SPH_MATCH_BOOLEAN = 3
  65. SPH_MATCH_EXTENDED = 4
  66. SPH_MATCH_FULLSCAN = 5
  67. SPH_MATCH_EXTENDED2 = 6
  68. # known ranking modes (extended2 mode only)
  69. SPH_RANK_PROXIMITY_BM25 = 0 # default mode, phrase proximity major factor and BM25 minor one
  70. SPH_RANK_BM25 = 1 # statistical mode, BM25 ranking only (faster but worse quality)
  71. SPH_RANK_NONE = 2 # no ranking, all matches get a weight of 1
  72. SPH_RANK_WORDCOUNT = 3 # simple word-count weighting, rank is a weighted sum of per-field keyword occurence counts
  73. SPH_RANK_PROXIMITY = 4
  74. SPH_RANK_MATCHANY = 5
  75. SPH_RANK_FIELDMASK = 6
  76. SPH_RANK_SPH04 = 7
  77. SPH_RANK_EXPR = 8
  78. SPH_RANK_TOTAL = 9
  79. # known sort modes
  80. SPH_SORT_RELEVANCE = 0
  81. SPH_SORT_ATTR_DESC = 1
  82. SPH_SORT_ATTR_ASC = 2
  83. SPH_SORT_TIME_SEGMENTS = 3
  84. SPH_SORT_EXTENDED = 4
  85. SPH_SORT_EXPR = 5
  86. # known filter types
  87. SPH_FILTER_VALUES = 0
  88. SPH_FILTER_RANGE = 1
  89. SPH_FILTER_FLOATRANGE = 2
  90. SPH_FILTER_STRING = 3
  91. SPH_FILTER_STRING_LIST = 6
  92. # known attribute types
  93. SPH_ATTR_NONE = 0
  94. SPH_ATTR_INTEGER = 1
  95. SPH_ATTR_TIMESTAMP = 2
  96. SPH_ATTR_ORDINAL = 3
  97. SPH_ATTR_BOOL = 4
  98. SPH_ATTR_FLOAT = 5
  99. SPH_ATTR_BIGINT = 6
  100. SPH_ATTR_STRING = 7
  101. SPH_ATTR_DOUBLE = 13
  102. SPH_ATTR_FACTORS = 1001
  103. SPH_ATTR_MULTI = long(0X40000001)
  104. SPH_ATTR_MULTI64 = long(0X40000002)
  105. SPH_ATTR_TYPES = (SPH_ATTR_NONE,
  106. SPH_ATTR_INTEGER,
  107. SPH_ATTR_TIMESTAMP,
  108. SPH_ATTR_ORDINAL,
  109. SPH_ATTR_BOOL,
  110. SPH_ATTR_FLOAT,
  111. SPH_ATTR_BIGINT,
  112. SPH_ATTR_STRING,
  113. SPH_ATTR_MULTI,
  114. SPH_ATTR_MULTI64)
  115. # known grouping functions
  116. SPH_GROUPBY_DAY = 0
  117. SPH_GROUPBY_WEEK = 1
  118. SPH_GROUPBY_MONTH = 2
  119. SPH_GROUPBY_YEAR = 3
  120. SPH_GROUPBY_ATTR = 4
  121. SPH_GROUPBY_ATTRPAIR = 5
  122. class SphinxClient:
  123. def __init__ (self):
  124. """
  125. Create a new client object, and fill defaults.
  126. """
  127. self._host = 'localhost' # searchd host (default is "localhost")
  128. self._port = 9312 # searchd port (default is 9312)
  129. self._path = None # searchd unix-domain socket path
  130. self._socket = None
  131. self._offset = 0 # how much records to seek from result-set start (default is 0)
  132. self._limit = 20 # how much records to return from result-set starting at offset (default is 20)
  133. self._mode = SPH_MATCH_EXTENDED2 # query matching mode (default is SPH_MATCH_EXTENDED2)
  134. self._weights = [] # per-field weights (default is 1 for all fields)
  135. self._sort = SPH_SORT_RELEVANCE # match sorting mode (default is SPH_SORT_RELEVANCE)
  136. self._sortby = bytearray() # attribute to sort by (defualt is "")
  137. self._min_id = 0 # min ID to match (default is 0)
  138. self._max_id = 0 # max ID to match (default is UINT_MAX)
  139. self._filters = [] # search filters
  140. self._groupby = bytearray() # group-by attribute name
  141. self._groupfunc = SPH_GROUPBY_DAY # group-by function (to pre-process group-by attribute value with)
  142. self._groupsort = str_bytes('@group desc') # group-by sorting clause (to sort groups in result set with)
  143. self._groupdistinct = bytearray() # group-by count-distinct attribute
  144. self._maxmatches = 1000 # max matches to retrieve
  145. self._cutoff = 0 # cutoff to stop searching at
  146. self._retrycount = 0 # distributed retry count
  147. self._retrydelay = 0 # distributed retry delay
  148. self._anchor = {} # geographical anchor point
  149. self._indexweights = {} # per-index weights
  150. self._ranker = SPH_RANK_PROXIMITY_BM25 # ranking mode
  151. self._rankexpr = bytearray() # ranking expression for SPH_RANK_EXPR
  152. self._maxquerytime = 0 # max query time, milliseconds (default is 0, do not limit)
  153. self._timeout = 1.0 # connection timeout
  154. self._fieldweights = {} # per-field-name weights
  155. self._overrides = {} # per-query attribute values overrides
  156. self._select = str_bytes('*') # select-list (attributes or expressions, with optional aliases)
  157. self._query_flags = SetBit ( 0, 6, True ) # default idf=tfidf_normalized
  158. self._predictedtime = 0 # per-query max_predicted_time
  159. self._outerorderby = bytearray() # outer match sort by
  160. self._outeroffset = 0 # outer offset
  161. self._outerlimit = 0 # outer limit
  162. self._hasouter = False # sub-select enabled
  163. self._tokenfilterlibrary = bytearray() # token_filter plugin library name
  164. self._tokenfiltername = bytearray() # token_filter plugin name
  165. self._tokenfilteropts = bytearray() # token_filter plugin options
  166. self._error = '' # last error message
  167. self._warning = '' # last warning message
  168. self._reqs = [] # requests array for multi-query
  169. def __del__ (self):
  170. if self._socket:
  171. self._socket.close()
  172. def GetLastError (self):
  173. """
  174. Get last error message (string).
  175. """
  176. return self._error
  177. def GetLastWarning (self):
  178. """
  179. Get last warning message (string).
  180. """
  181. return self._warning
  182. def SetServer (self, host, port = None):
  183. """
  184. Set searchd server host and port.
  185. """
  186. assert(isinstance(host, str))
  187. if host.startswith('/'):
  188. self._path = host
  189. return
  190. elif host.startswith('unix://'):
  191. self._path = host[7:]
  192. return
  193. self._host = host
  194. if isinstance(port, int):
  195. assert(port>0 and port<65536)
  196. self._port = port
  197. self._path = None
  198. def SetConnectTimeout ( self, timeout ):
  199. """
  200. Set connection timeout ( float second )
  201. """
  202. assert (isinstance(timeout, float))
  203. # set timeout to 0 make connaection non-blocking that is wrong so timeout got clipped to reasonable minimum
  204. self._timeout = max ( 0.001, timeout )
  205. def _Connect (self):
  206. """
  207. INTERNAL METHOD, DO NOT CALL. Connects to searchd server.
  208. """
  209. if self._socket:
  210. # we have a socket, but is it still alive?
  211. sr, sw, _ = select.select ( [self._socket], [self._socket], [], 0 )
  212. # this is how alive socket should look
  213. if len(sr)==0 and len(sw)==1:
  214. return self._socket
  215. # oops, looks like it was closed, lets reopen
  216. self._socket.close()
  217. self._socket = None
  218. try:
  219. if self._path:
  220. af = socket.AF_UNIX
  221. addr = self._path
  222. desc = self._path
  223. else:
  224. af = socket.AF_INET
  225. addr = ( self._host, self._port )
  226. desc = '%s;%s' % addr
  227. sock = socket.socket ( af, socket.SOCK_STREAM )
  228. sock.settimeout ( self._timeout )
  229. sock.connect ( addr )
  230. except socket.error as msg:
  231. if sock:
  232. sock.close()
  233. self._error = 'connection to %s failed (%s)' % ( desc, msg )
  234. return
  235. v = unpack('>L', sock.recv(4))[0]
  236. if v<1:
  237. sock.close()
  238. self._error = 'expected searchd protocol version, got %s' % v
  239. return
  240. # all ok, send my version
  241. sock.send(pack('>L', 1))
  242. return sock
  243. def _GetResponse (self, sock, client_ver):
  244. """
  245. INTERNAL METHOD, DO NOT CALL. Gets and checks response packet from searchd server.
  246. """
  247. (status, ver, length) = unpack('>2HL', sock.recv(8))
  248. response = bytearray()
  249. left = length
  250. while left>0:
  251. chunk = sock.recv(left)
  252. if chunk:
  253. response += chunk
  254. left -= len(chunk)
  255. else:
  256. break
  257. if not self._socket:
  258. sock.close()
  259. # check response
  260. read = len(response)
  261. if not response or read!=length:
  262. if length:
  263. self._error = 'failed to read searchd response (status=%s, ver=%s, len=%s, read=%s)' \
  264. % (status, ver, length, read)
  265. else:
  266. self._error = 'received zero-sized searchd response'
  267. return None
  268. # check status
  269. if status==SEARCHD_WARNING:
  270. wend = 4 + unpack ( '>L', response[0:4] )[0]
  271. self._warning = bytes_str(response[4:wend])
  272. return response[wend:]
  273. if status==SEARCHD_ERROR:
  274. self._error = 'searchd error: ' + bytes_str(response[4:])
  275. return None
  276. if status==SEARCHD_RETRY:
  277. self._error = 'temporary searchd error: ' + bytes_str(response[4:])
  278. return None
  279. if status!=SEARCHD_OK:
  280. self._error = 'unknown status code %d' % status
  281. return None
  282. # check version
  283. if ver<client_ver:
  284. self._warning = 'searchd command v.%d.%d older than client\'s v.%d.%d, some options might not work' \
  285. % (ver>>8, ver&0xff, client_ver>>8, client_ver&0xff)
  286. return response
  287. def _Send ( self, sock, req ):
  288. """
  289. INTERNAL METHOD, DO NOT CALL. send request to searchd server.
  290. """
  291. total = 0
  292. while True:
  293. sent = sock.send ( req[total:] )
  294. if sent<=0:
  295. break
  296. total = total + sent
  297. return total
  298. def SetLimits (self, offset, limit, maxmatches=0, cutoff=0):
  299. """
  300. Set offset and count into result set, and optionally set max-matches and cutoff limits.
  301. """
  302. assert ( type(offset) in [int,long] and 0<=offset<16777216 )
  303. assert ( type(limit) in [int,long] and 0<limit<16777216 )
  304. assert(maxmatches>=0)
  305. self._offset = offset
  306. self._limit = limit
  307. if maxmatches>0:
  308. self._maxmatches = maxmatches
  309. if cutoff>=0:
  310. self._cutoff = cutoff
  311. def SetMaxQueryTime (self, maxquerytime):
  312. """
  313. Set maximum query time, in milliseconds, per-index. 0 means 'do not limit'.
  314. """
  315. assert(isinstance(maxquerytime,int) and maxquerytime>0)
  316. self._maxquerytime = maxquerytime
  317. def SetMatchMode (self, mode):
  318. """
  319. Set matching mode.
  320. print('DEPRECATED: Do not call this method or, even better, use SphinxQL instead of an API', file=sys.stderr)
  321. """
  322. assert(mode in [SPH_MATCH_ALL, SPH_MATCH_ANY, SPH_MATCH_PHRASE, SPH_MATCH_BOOLEAN, SPH_MATCH_EXTENDED, SPH_MATCH_FULLSCAN, SPH_MATCH_EXTENDED2])
  323. self._mode = mode
  324. def SetRankingMode ( self, ranker, rankexpr='' ):
  325. """
  326. Set ranking mode.
  327. """
  328. assert(ranker>=0 and ranker<SPH_RANK_TOTAL)
  329. self._ranker = ranker
  330. self._rankexpr = str_bytes(rankexpr)
  331. def SetSortMode ( self, mode, clause='' ):
  332. """
  333. Set sorting mode.
  334. """
  335. assert ( mode in [SPH_SORT_RELEVANCE, SPH_SORT_ATTR_DESC, SPH_SORT_ATTR_ASC, SPH_SORT_TIME_SEGMENTS, SPH_SORT_EXTENDED, SPH_SORT_EXPR] )
  336. assert ( isinstance ( clause, (str,text_type) ) )
  337. self._sort = mode
  338. self._sortby = str_bytes(clause)
  339. def SetFieldWeights (self, weights):
  340. """
  341. Bind per-field weights by name; expects (name,field_weight) dictionary as argument.
  342. """
  343. assert(isinstance(weights,dict))
  344. for key,val in list(weights.items()):
  345. assert(isinstance(key,str))
  346. AssertUInt32 ( val )
  347. self._fieldweights = weights
  348. def SetIndexWeights (self, weights):
  349. """
  350. Bind per-index weights by name; expects (name,index_weight) dictionary as argument.
  351. """
  352. assert(isinstance(weights,dict))
  353. for key,val in list(weights.items()):
  354. assert(isinstance(key,str))
  355. AssertUInt32(val)
  356. self._indexweights = weights
  357. def SetIDRange (self, minid, maxid):
  358. """
  359. Set IDs range to match.
  360. Only match records if document ID is beetwen $min and $max (inclusive).
  361. """
  362. assert(isinstance(minid, (int, long)))
  363. assert(isinstance(maxid, (int, long)))
  364. assert(minid<=maxid)
  365. self._min_id = minid
  366. self._max_id = maxid
  367. def SetFilter ( self, attribute, values, exclude=0 ):
  368. """
  369. Set values set filter.
  370. Only match records where 'attribute' value is in given 'values' set.
  371. """
  372. assert(isinstance(attribute, str))
  373. assert iter(values)
  374. for value in values:
  375. AssertInt32 ( value )
  376. self._filters.append ( { 'type':SPH_FILTER_VALUES, 'attr':attribute, 'exclude':exclude, 'values':values } )
  377. def SetFilterString ( self, attribute, value, exclude=0 ):
  378. """
  379. Set string filter.
  380. Only match records where 'attribute' value is equal
  381. """
  382. assert(isinstance(attribute, str))
  383. assert(isinstance(value, str))
  384. self._filters.append ( { 'type':SPH_FILTER_STRING, 'attr':attribute, 'exclude':exclude, 'value':value } )
  385. def SetFilterStringList ( self, attribute, value, exclude=0 ):
  386. """
  387. Set string list filter.
  388. """
  389. assert(isinstance(attribute, str))
  390. assert(iter(value))
  391. for v in value:
  392. assert(isinstance(v, str))
  393. self._filters.append ( { 'type':SPH_FILTER_STRING_LIST, 'attr':attribute, 'exclude':exclude, 'values':value } )
  394. def SetFilterRange (self, attribute, min_, max_, exclude=0 ):
  395. """
  396. Set range filter.
  397. Only match records if 'attribute' value is beetwen 'min_' and 'max_' (inclusive).
  398. """
  399. assert(isinstance(attribute, str))
  400. AssertInt32(min_)
  401. AssertInt32(max_)
  402. assert(min_<=max_)
  403. self._filters.append ( { 'type':SPH_FILTER_RANGE, 'attr':attribute, 'exclude':exclude, 'min':min_, 'max':max_ } )
  404. def SetFilterFloatRange (self, attribute, min_, max_, exclude=0 ):
  405. assert(isinstance(attribute,str))
  406. assert(isinstance(min_,float))
  407. assert(isinstance(max_,float))
  408. assert(min_ <= max_)
  409. self._filters.append ( {'type':SPH_FILTER_FLOATRANGE, 'attr':attribute, 'exclude':exclude, 'min':min_, 'max':max_} )
  410. def SetGeoAnchor (self, attrlat, attrlong, latitude, longitude):
  411. assert(isinstance(attrlat,str))
  412. assert(isinstance(attrlong,str))
  413. assert(isinstance(latitude,float))
  414. assert(isinstance(longitude,float))
  415. self._anchor['attrlat'] = attrlat
  416. self._anchor['attrlong'] = attrlong
  417. self._anchor['lat'] = latitude
  418. self._anchor['long'] = longitude
  419. def SetGroupBy ( self, attribute, func, groupsort='@group desc' ):
  420. """
  421. Set grouping attribute and function.
  422. """
  423. assert(isinstance(attribute, (str,text_type)))
  424. assert(func in [SPH_GROUPBY_DAY, SPH_GROUPBY_WEEK, SPH_GROUPBY_MONTH, SPH_GROUPBY_YEAR, SPH_GROUPBY_ATTR, SPH_GROUPBY_ATTRPAIR] )
  425. assert(isinstance(groupsort, (str,text_type)))
  426. self._groupby = str_bytes(attribute)
  427. self._groupfunc = func
  428. self._groupsort = str_bytes(groupsort)
  429. def SetGroupDistinct (self, attribute):
  430. assert(isinstance(attribute,(str,text_type)))
  431. self._groupdistinct = str_bytes(attribute)
  432. def SetRetries (self, count, delay=0):
  433. assert(isinstance(count,int) and count>=0)
  434. assert(isinstance(delay,int) and delay>=0)
  435. self._retrycount = count
  436. self._retrydelay = delay
  437. def SetOverride (self, name, type, values):
  438. """
  439. print('DEPRECATED: Do not call this method. Use SphinxQL REMAP() function instead.', file=sys.stderr)
  440. """
  441. assert(isinstance(name, str))
  442. assert(type in SPH_ATTR_TYPES)
  443. assert(isinstance(values, dict))
  444. self._overrides[name] = {'name': name, 'type': type, 'values': values}
  445. def SetSelect (self, select):
  446. assert(isinstance(select, (str,text_type)))
  447. self._select = str_bytes(select)
  448. def SetQueryFlag ( self, name, value ):
  449. known_names = [ "reverse_scan", "sort_method", "max_predicted_time", "boolean_simplify", "idf", "global_idf" ]
  450. flags = { "reverse_scan":[0, 1], "sort_method":["pq", "kbuffer"],"max_predicted_time":[0], "boolean_simplify":[True, False], "idf":["normalized", "plain", "tfidf_normalized", "tfidf_unnormalized"], "global_idf":[True, False] }
  451. assert ( name in known_names )
  452. assert ( value in flags[name] or ( name=="max_predicted_time" and isinstance(value, (int, long)) and value>=0))
  453. if name=="reverse_scan":
  454. self._query_flags = SetBit ( self._query_flags, 0, value==1 )
  455. if name=="sort_method":
  456. self._query_flags = SetBit ( self._query_flags, 1, value=="kbuffer" )
  457. if name=="max_predicted_time":
  458. self._query_flags = SetBit ( self._query_flags, 2, value>0 )
  459. self._predictedtime = int(value)
  460. if name=="boolean_simplify":
  461. self._query_flags= SetBit ( self._query_flags, 3, value )
  462. if name=="idf" and ( value=="plain" or value=="normalized" ) :
  463. self._query_flags = SetBit ( self._query_flags, 4, value=="plain" )
  464. if name=="global_idf":
  465. self._query_flags= SetBit ( self._query_flags, 5, value )
  466. if name=="idf" and ( value=="tfidf_normalized" or value=="tfidf_unnormalized" ) :
  467. self._query_flags = SetBit ( self._query_flags, 6, value=="tfidf_normalized" )
  468. def SetOuterSelect ( self, orderby, offset, limit ):
  469. assert(isinstance(orderby, (str,text_type)))
  470. assert(isinstance(offset, (int, long)))
  471. assert(isinstance(limit, (int, long)))
  472. assert ( offset>=0 )
  473. assert ( limit>0 )
  474. self._outerorderby = str_bytes(orderby)
  475. self._outeroffset = offset
  476. self._outerlimit = limit
  477. self._hasouter = True
  478. def SetTokenFilter ( self, library, name, opts='' ):
  479. assert(isinstance(library, str))
  480. assert(isinstance(name, str))
  481. assert(isinstance(opts, str))
  482. self._tokenfilterlibrary = str_bytes(library)
  483. self._tokenfiltername = str_bytes(name)
  484. self._tokenfilteropts = str_bytes(opts)
  485. def ResetOverrides (self):
  486. self._overrides = {}
  487. def ResetFilters (self):
  488. """
  489. Clear all filters (for multi-queries).
  490. """
  491. self._filters = []
  492. self._anchor = {}
  493. def ResetGroupBy (self):
  494. """
  495. Clear groupby settings (for multi-queries).
  496. """
  497. self._groupby = bytearray()
  498. self._groupfunc = SPH_GROUPBY_DAY
  499. self._groupsort = str_bytes('@group desc')
  500. self._groupdistinct = bytearray()
  501. def ResetQueryFlag (self):
  502. self._query_flags = SetBit ( 0, 6, True ) # default idf=tfidf_normalized
  503. self._predictedtime = 0
  504. def ResetOuterSelect (self):
  505. self._outerorderby = bytearray()
  506. self._outeroffset = 0
  507. self._outerlimit = 0
  508. self._hasouter = False
  509. def Query (self, query, index='*', comment=''):
  510. """
  511. Connect to searchd server and run given search query.
  512. Returns None on failure; result set hash on success (see documentation for details).
  513. """
  514. assert(len(self._reqs)==0)
  515. self.AddQuery(query,index,comment)
  516. results = self.RunQueries()
  517. self._reqs = [] # we won't re-run erroneous batch
  518. if not results or len(results)==0:
  519. return None
  520. self._error = results[0]['error']
  521. self._warning = results[0]['warning']
  522. if results[0]['status'] == SEARCHD_ERROR:
  523. return None
  524. return results[0]
  525. def AddQuery (self, query, index='*', comment=''):
  526. """
  527. Add query to batch.
  528. """
  529. # build request
  530. req = bytearray()
  531. req.extend(pack('>5L', self._query_flags, self._offset, self._limit, self._mode, self._ranker))
  532. if self._ranker==SPH_RANK_EXPR:
  533. req.extend(pack('>L', len(self._rankexpr)))
  534. req.extend(self._rankexpr)
  535. req.extend(pack('>L', self._sort))
  536. req.extend(pack('>L', len(self._sortby)))
  537. req.extend(self._sortby)
  538. query = str_bytes(query)
  539. assert(isinstance(query,bytearray))
  540. req.extend(pack('>L', len(query)))
  541. req.extend(query)
  542. req.extend(pack('>L', len(self._weights)))
  543. for w in self._weights:
  544. req.extend(pack('>L', w))
  545. index = str_bytes(index)
  546. assert(isinstance(index,bytearray))
  547. req.extend(pack('>L', len(index)))
  548. req.extend(index)
  549. req.extend(pack('>L',1)) # id64 range marker
  550. req.extend(pack('>Q', self._min_id))
  551. req.extend(pack('>Q', self._max_id))
  552. # filters
  553. req.extend ( pack ( '>L', len(self._filters) ) )
  554. for f in self._filters:
  555. attr = str_bytes(f['attr'])
  556. req.extend ( pack ( '>L', len(f['attr'])) + attr)
  557. filtertype = f['type']
  558. req.extend ( pack ( '>L', filtertype))
  559. if filtertype == SPH_FILTER_VALUES:
  560. req.extend ( pack ('>L', len(f['values'])))
  561. for val in f['values']:
  562. req.extend ( pack ('>q', val))
  563. elif filtertype == SPH_FILTER_RANGE:
  564. req.extend ( pack ('>2q', f['min'], f['max']))
  565. elif filtertype == SPH_FILTER_FLOATRANGE:
  566. req.extend ( pack ('>2f', f['min'], f['max']))
  567. elif filtertype == SPH_FILTER_STRING:
  568. val = str_bytes(f['value'])
  569. req.extend ( pack ( '>L', len(val) ) )
  570. req.extend ( val )
  571. elif filtertype == SPH_FILTER_STRING_LIST:
  572. req.extend ( pack ('>L', len(f['values'])))
  573. for sval in f['values']:
  574. val = str_bytes( sval )
  575. req.extend ( pack ( '>L', len(val) ) )
  576. req.extend(val)
  577. req.extend ( pack ( '>L', f['exclude'] ) )
  578. # group-by, max-matches, group-sort
  579. req.extend ( pack ( '>2L', self._groupfunc, len(self._groupby) ) )
  580. req.extend ( self._groupby )
  581. req.extend ( pack ( '>2L', self._maxmatches, len(self._groupsort) ) )
  582. req.extend ( self._groupsort )
  583. req.extend ( pack ( '>LLL', self._cutoff, self._retrycount, self._retrydelay))
  584. req.extend ( pack ( '>L', len(self._groupdistinct)))
  585. req.extend ( self._groupdistinct)
  586. # anchor point
  587. if len(self._anchor) == 0:
  588. req.extend ( pack ('>L', 0))
  589. else:
  590. attrlat, attrlong = str_bytes(self._anchor['attrlat']), str_bytes(self._anchor['attrlong'])
  591. latitude, longitude = self._anchor['lat'], self._anchor['long']
  592. req.extend ( pack ('>L', 1))
  593. req.extend ( pack ('>L', len(attrlat)) + attrlat)
  594. req.extend ( pack ('>L', len(attrlong)) + attrlong)
  595. req.extend ( pack ('>f', latitude) + pack ('>f', longitude))
  596. # per-index weights
  597. req.extend ( pack ('>L',len(self._indexweights)))
  598. for indx,weight in list(self._indexweights.items()):
  599. indx = str_bytes(indx)
  600. req.extend ( pack ('>L',len(indx)) + indx + pack ('>L',weight))
  601. # max query time
  602. req.extend ( pack ('>L', self._maxquerytime) )
  603. # per-field weights
  604. req.extend ( pack ('>L',len(self._fieldweights) ) )
  605. for field,weight in list(self._fieldweights.items()):
  606. field = str_bytes(field)
  607. req.extend ( pack ('>L',len(field)) + field + pack ('>L',weight) )
  608. # comment
  609. comment = str_bytes(comment)
  610. req.extend ( pack('>L',len(comment)) + comment )
  611. # attribute overrides
  612. req.extend ( pack('>L', len(self._overrides)) )
  613. for v in list(self._overrides.values()):
  614. name = str_bytes(v['name'])
  615. req.extend ( ( pack('>L', len(name)), name ) )
  616. req.extend ( pack('>LL', v['type'], len(v['values'])) )
  617. for id, value in v['values'].items():
  618. req.extend ( pack('>Q', id) )
  619. if v['type'] == SPH_ATTR_FLOAT:
  620. req.extend ( pack('>f', value) )
  621. elif v['type'] == SPH_ATTR_BIGINT:
  622. req.extend ( pack('>q', value) )
  623. else:
  624. req.extend ( pack('>l', value) )
  625. # select-list
  626. req.extend ( pack('>L', len(self._select)) )
  627. req.extend ( self._select )
  628. if self._predictedtime>0:
  629. req.extend ( pack('>L', self._predictedtime ) )
  630. # outer
  631. req.extend ( pack('>L',len(self._outerorderby)) + self._outerorderby )
  632. req.extend ( pack ( '>2L', self._outeroffset, self._outerlimit ) )
  633. if self._hasouter:
  634. req.extend ( pack('>L', 1) )
  635. else:
  636. req.extend ( pack('>L', 0) )
  637. # token_filter
  638. req.extend ( pack('>L',len(self._tokenfilterlibrary)) + self._tokenfilterlibrary )
  639. req.extend ( pack('>L',len(self._tokenfiltername)) + self._tokenfiltername )
  640. req.extend ( pack('>L',len(self._tokenfilteropts)) + self._tokenfilteropts )
  641. # send query, get response
  642. self._reqs.append(req)
  643. return
  644. def RunQueries (self):
  645. """
  646. Run queries batch.
  647. Returns None on network IO failure; or an array of result set hashes on success.
  648. """
  649. if len(self._reqs)==0:
  650. self._error = 'no queries defined, issue AddQuery() first'
  651. return None
  652. sock = self._Connect()
  653. if not sock:
  654. return None
  655. req = bytearray()
  656. for r in self._reqs:
  657. req.extend(r)
  658. length = len(req)+8
  659. req_all = bytearray()
  660. req_all.extend(pack('>HHLLL', SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, length, 0, len(self._reqs)))
  661. req_all.extend(req)
  662. self._Send ( sock, req_all )
  663. response = self._GetResponse(sock, VER_COMMAND_SEARCH)
  664. if not response:
  665. return None
  666. nreqs = len(self._reqs)
  667. # parse response
  668. max_ = len(response)
  669. p = 0
  670. results = []
  671. for i in range(0,nreqs,1):
  672. result = {}
  673. results.append(result)
  674. result['error'] = ''
  675. result['warning'] = ''
  676. status = unpack('>L', response[p:p+4])[0]
  677. p += 4
  678. result['status'] = status
  679. if status != SEARCHD_OK:
  680. length = unpack('>L', response[p:p+4])[0]
  681. p += 4
  682. message = bytes_str(response[p:p+length])
  683. p += length
  684. if status == SEARCHD_WARNING:
  685. result['warning'] = message
  686. else:
  687. result['error'] = message
  688. continue
  689. # read schema
  690. fields = []
  691. attrs = []
  692. nfields = unpack('>L', response[p:p+4])[0]
  693. p += 4
  694. while nfields>0 and p<max_:
  695. nfields -= 1
  696. length = unpack('>L', response[p:p+4])[0]
  697. p += 4
  698. fields.append(bytes_str(response[p:p+length]))
  699. p += length
  700. result['fields'] = fields
  701. nattrs = unpack('>L', response[p:p+4])[0]
  702. p += 4
  703. while nattrs>0 and p<max_:
  704. nattrs -= 1
  705. length = unpack('>L', response[p:p+4])[0]
  706. p += 4
  707. attr = bytes_str(response[p:p+length])
  708. p += length
  709. type_ = unpack('>L', response[p:p+4])[0]
  710. p += 4
  711. attrs.append([attr,type_])
  712. result['attrs'] = attrs
  713. # read match count
  714. count = unpack('>L', response[p:p+4])[0]
  715. p += 4
  716. id64 = unpack('>L', response[p:p+4])[0]
  717. p += 4
  718. # read matches
  719. result['matches'] = []
  720. while count>0 and p<max_:
  721. count -= 1
  722. if id64:
  723. doc, weight = unpack('>QL', response[p:p+12])
  724. p += 12
  725. else:
  726. doc, weight = unpack('>2L', response[p:p+8])
  727. p += 8
  728. match = { 'id':doc, 'weight':weight, 'attrs':{} }
  729. for i in range(len(attrs)):
  730. if attrs[i][1] == SPH_ATTR_FLOAT:
  731. match['attrs'][attrs[i][0]] = unpack('>f', response[p:p+4])[0]
  732. elif attrs[i][1] == SPH_ATTR_BIGINT:
  733. match['attrs'][attrs[i][0]] = unpack('>q', response[p:p+8])[0]
  734. p += 4
  735. elif attrs[i][1] == SPH_ATTR_STRING:
  736. slen = unpack('>L', response[p:p+4])[0]
  737. p += 4
  738. match['attrs'][attrs[i][0]] = ''
  739. if slen>0:
  740. match['attrs'][attrs[i][0]] = bytes_str(response[p:p+slen])
  741. p += slen-4
  742. elif attrs[i][1] == SPH_ATTR_FACTORS:
  743. slen = unpack('>L', response[p:p+4])[0]
  744. p += 4
  745. match['attrs'][attrs[i][0]] = ''
  746. if slen>0:
  747. match['attrs'][attrs[i][0]] = response[p:p+slen-4]
  748. p += slen-4
  749. p -= 4
  750. elif attrs[i][1] == SPH_ATTR_MULTI:
  751. match['attrs'][attrs[i][0]] = []
  752. nvals = unpack('>L', response[p:p+4])[0]
  753. p += 4
  754. for n in range(0,nvals,1):
  755. match['attrs'][attrs[i][0]].append(unpack('>L', response[p:p+4])[0])
  756. p += 4
  757. p -= 4
  758. elif attrs[i][1] == SPH_ATTR_MULTI64:
  759. match['attrs'][attrs[i][0]] = []
  760. nvals = unpack('>L', response[p:p+4])[0]
  761. nvals = nvals/2
  762. p += 4
  763. for n in range(0,nvals,1):
  764. match['attrs'][attrs[i][0]].append(unpack('>q', response[p:p+8])[0])
  765. p += 8
  766. p -= 4
  767. else:
  768. match['attrs'][attrs[i][0]] = unpack('>L', response[p:p+4])[0]
  769. p += 4
  770. result['matches'].append ( match )
  771. result['total'], result['total_found'], result['time'], words = unpack('>4L', response[p:p+16])
  772. result['time'] = '%.3f' % (result['time']/1000.0)
  773. p += 16
  774. result['words'] = []
  775. while words>0:
  776. words -= 1
  777. length = unpack('>L', response[p:p+4])[0]
  778. p += 4
  779. word = bytes_str(response[p:p+length])
  780. p += length
  781. docs, hits = unpack('>2L', response[p:p+8])
  782. p += 8
  783. result['words'].append({'word':word, 'docs':docs, 'hits':hits})
  784. self._reqs = []
  785. return results
  786. def BuildExcerpts (self, docs, index, words, opts=None):
  787. """
  788. Connect to searchd server and generate exceprts from given documents.
  789. """
  790. if not opts:
  791. opts = {}
  792. assert(isinstance(docs, list))
  793. assert(isinstance(index, (str,text_type)))
  794. assert(isinstance(words, (str,text_type)))
  795. assert(isinstance(opts, dict))
  796. sock = self._Connect()
  797. if not sock:
  798. return None
  799. # fixup options
  800. opts.setdefault('before_match', '<b>')
  801. opts.setdefault('after_match', '</b>')
  802. opts.setdefault('chunk_separator', ' ... ')
  803. opts.setdefault('html_strip_mode', 'index')
  804. opts.setdefault('limit', 256)
  805. opts.setdefault('limit_passages', 0)
  806. opts.setdefault('limit_words', 0)
  807. opts.setdefault('around', 5)
  808. opts.setdefault('start_passage_id', 1)
  809. opts.setdefault('passage_boundary', 'none')
  810. # build request
  811. # v.1.0 req
  812. flags = 1 # (remove spaces)
  813. if opts.get('exact_phrase'): flags |= 2
  814. if opts.get('single_passage'): flags |= 4
  815. if opts.get('use_boundaries'): flags |= 8
  816. if opts.get('weight_order'): flags |= 16
  817. if opts.get('query_mode'): flags |= 32
  818. if opts.get('force_all_words'): flags |= 64
  819. if opts.get('load_files'): flags |= 128
  820. if opts.get('allow_empty'): flags |= 256
  821. if opts.get('emit_zones'): flags |= 512
  822. if opts.get('load_files_scattered'): flags |= 1024
  823. # mode=0, flags
  824. req = bytearray()
  825. req.extend(pack('>2L', 0, flags))
  826. # req index
  827. index = str_bytes(index)
  828. req.extend(pack('>L', len(index)))
  829. req.extend(index)
  830. # req words
  831. words = str_bytes(words)
  832. req.extend(pack('>L', len(words)))
  833. req.extend(words)
  834. # options
  835. opts_before_match = str_bytes(opts['before_match'])
  836. req.extend(pack('>L', len(opts_before_match)))
  837. req.extend(opts_before_match)
  838. opts_after_match = str_bytes(opts['after_match'])
  839. req.extend(pack('>L', len(opts_after_match)))
  840. req.extend(opts_after_match)
  841. opts_chunk_separator = str_bytes(opts['chunk_separator'])
  842. req.extend(pack('>L', len(opts_chunk_separator)))
  843. req.extend(opts_chunk_separator)
  844. req.extend(pack('>L', int(opts['limit'])))
  845. req.extend(pack('>L', int(opts['around'])))
  846. req.extend(pack('>L', int(opts['limit_passages'])))
  847. req.extend(pack('>L', int(opts['limit_words'])))
  848. req.extend(pack('>L', int(opts['start_passage_id'])))
  849. opts_html_strip_mode = str_bytes(opts['html_strip_mode'])
  850. req.extend(pack('>L', len(opts_html_strip_mode)))
  851. req.extend(opts_html_strip_mode)
  852. opts_passage_boundary = str_bytes(opts['passage_boundary'])
  853. req.extend(pack('>L', len(opts_passage_boundary)))
  854. req.extend(opts_passage_boundary)
  855. # documents
  856. req.extend(pack('>L', len(docs)))
  857. for doc in docs:
  858. doc = str_bytes(doc)
  859. req.extend(pack('>L', len(doc)))
  860. req.extend(doc)
  861. # send query, get response
  862. length = len(req)
  863. # add header
  864. req_head = bytearray()
  865. req_head.extend(pack('>2HL', SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, length))
  866. req_all = req_head + req
  867. self._Send ( sock, req_all )
  868. response = self._GetResponse(sock, VER_COMMAND_EXCERPT )
  869. if not response:
  870. return []
  871. # parse response
  872. pos = 0
  873. res = []
  874. rlen = len(response)
  875. for i in range(len(docs)):
  876. length = unpack('>L', response[pos:pos+4])[0]
  877. pos += 4
  878. if pos+length > rlen:
  879. self._error = 'incomplete reply'
  880. return []
  881. res.append(bytes_str(response[pos:pos+length]))
  882. pos += length
  883. return res
  884. def UpdateAttributes ( self, index, attrs, values, mva=False, ignorenonexistent=False ):
  885. """
  886. Update given attribute values on given documents in given indexes.
  887. Returns amount of updated documents (0 or more) on success, or -1 on failure.
  888. 'attrs' must be a list of strings.
  889. 'values' must be a dict with int key (document ID) and list of int values (new attribute values).
  890. optional boolean parameter 'mva' points that there is update of MVA attributes.
  891. In this case the 'values' must be a dict with int key (document ID) and list of lists of int values
  892. (new MVA attribute values).
  893. Optional boolean parameter 'ignorenonexistent' points that the update will silently ignore any warnings about
  894. trying to update a column which is not exists in current index schema.
  895. Example:
  896. res = cl.UpdateAttributes ( 'test1', [ 'group_id', 'date_added' ], { 2:[123,1000000000], 4:[456,1234567890] } )
  897. """
  898. assert ( isinstance ( index, str ) )
  899. assert ( isinstance ( attrs, list ) )
  900. assert ( isinstance ( values, dict ) )
  901. for attr in attrs:
  902. assert ( isinstance ( attr, str ) )
  903. for docid, entry in list(values.items()):
  904. AssertUInt32(docid)
  905. assert ( isinstance ( entry, list ) )
  906. assert ( len(attrs)==len(entry) )
  907. for val in entry:
  908. if mva:
  909. assert ( isinstance ( val, list ) )
  910. for vals in val:
  911. AssertInt32(vals)
  912. else:
  913. AssertInt32(val)
  914. # build request
  915. req = bytearray()
  916. index = str_bytes(index)
  917. req.extend( pack('>L',len(index)) + index )
  918. req.extend ( pack('>L',len(attrs)) )
  919. ignore_absent = 0
  920. if ignorenonexistent: ignore_absent = 1
  921. req.extend ( pack('>L', ignore_absent ) )
  922. mva_attr = 0
  923. if mva: mva_attr = 1
  924. for attr in attrs:
  925. attr = str_bytes(attr)
  926. req.extend ( pack('>L',len(attr)) + attr )
  927. req.extend ( pack('>L', mva_attr ) )
  928. req.extend ( pack('>L',len(values)) )
  929. for docid, entry in list(values.items()):
  930. req.extend ( pack('>Q',docid) )
  931. for val in entry:
  932. val_len = val
  933. if mva: val_len = len ( val )
  934. req.extend ( pack('>L',val_len ) )
  935. if mva:
  936. for vals in val:
  937. req.extend ( pack ('>L',vals) )
  938. # connect, send query, get response
  939. sock = self._Connect()
  940. if not sock:
  941. return None
  942. length = len(req)
  943. req_all = bytearray()
  944. req_all.extend( pack ( '>2HL', SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, length ) )
  945. req_all.extend( req )
  946. self._Send ( sock, req_all )
  947. response = self._GetResponse ( sock, VER_COMMAND_UPDATE )
  948. if not response:
  949. return -1
  950. # parse response
  951. updated = unpack ( '>L', response[0:4] )[0]
  952. return updated
  953. def BuildKeywords ( self, query, index, hits ):
  954. """
  955. Connect to searchd server, and generate keywords list for a given query.
  956. Returns None on failure, or a list of keywords on success.
  957. """
  958. assert ( isinstance ( query, str ) )
  959. assert ( isinstance ( index, str ) )
  960. assert ( isinstance ( hits, int ) )
  961. # build request
  962. req = bytearray()
  963. query = str_bytes(query)
  964. req.extend(pack ( '>L', len(query) ) + query)
  965. index = str_bytes(index)
  966. req.extend ( pack ( '>L', len(index) ) + index )
  967. req.extend ( pack ( '>L', hits ) )
  968. # connect, send query, get response
  969. sock = self._Connect()
  970. if not sock:
  971. return None
  972. length = len(req)
  973. req_all = bytearray()
  974. req_all.extend(pack ( '>2HL', SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, length ))
  975. req_all.extend(req)
  976. self._Send ( sock, req_all )
  977. response = self._GetResponse ( sock, VER_COMMAND_KEYWORDS )
  978. if not response:
  979. return None
  980. # parse response
  981. res = []
  982. nwords = unpack ( '>L', response[0:4] )[0]
  983. p = 4
  984. max_ = len(response)
  985. while nwords>0 and p<max_:
  986. nwords -= 1
  987. length = unpack ( '>L', response[p:p+4] )[0]
  988. p += 4
  989. tokenized = response[p:p+length]
  990. p += length
  991. length = unpack ( '>L', response[p:p+4] )[0]
  992. p += 4
  993. normalized = response[p:p+length]
  994. p += length
  995. entry = { 'tokenized':bytes_str(tokenized), 'normalized':bytes_str(normalized) }
  996. if hits:
  997. entry['docs'], entry['hits'] = unpack ( '>2L', response[p:p+8] )
  998. p += 8
  999. res.append ( entry )
  1000. if nwords>0 or p>max_:
  1001. self._error = 'incomplete reply'
  1002. return None
  1003. return res
  1004. def Status ( self, session=False ):
  1005. """
  1006. Get the status
  1007. """
  1008. # connect, send query, get response
  1009. sock = self._Connect()
  1010. if not sock:
  1011. return None
  1012. sess = 1
  1013. if session:
  1014. sess = 0
  1015. req = pack ( '>2HLL', SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, sess )
  1016. self._Send ( sock, req )
  1017. response = self._GetResponse ( sock, VER_COMMAND_STATUS )
  1018. if not response:
  1019. return None
  1020. # parse response
  1021. res = []
  1022. p = 8
  1023. max_ = len(response)
  1024. while p<max_:
  1025. length = unpack ( '>L', response[p:p+4] )[0]
  1026. k = response[p+4:p+length+4]
  1027. p += 4+length
  1028. length = unpack ( '>L', response[p:p+4] )[0]
  1029. v = response[p+4:p+length+4]
  1030. p += 4+length
  1031. res += [[bytes_str(k), bytes_str(v)]]
  1032. return res
  1033. ### persistent connections
  1034. def Open(self):
  1035. if self._socket:
  1036. self._error = 'already connected'
  1037. return None
  1038. server = self._Connect()
  1039. if not server:
  1040. return None
  1041. # command, command version = 0, body length = 4, body = 1
  1042. request = pack ( '>hhII', SEARCHD_COMMAND_PERSIST, 0, 4, 1 )
  1043. self._Send ( server, request )
  1044. self._socket = server
  1045. return True
  1046. def Close(self):
  1047. if not self._socket:
  1048. self._error = 'not connected'
  1049. return
  1050. self._socket.close()
  1051. self._socket = None
  1052. def EscapeString(self, string):
  1053. return re.sub(r"([=\(\)|\-!@~\"&/\\\^\$\=\<])", r"\\\1", string)
  1054. def FlushAttributes(self):
  1055. sock = self._Connect()
  1056. if not sock:
  1057. return -1
  1058. request = pack ( '>hhI', SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0 ) # cmd, ver, bodylen
  1059. self._Send ( sock, request )
  1060. response = self._GetResponse ( sock, VER_COMMAND_FLUSHATTRS )
  1061. if not response or len(response)!=4:
  1062. self._error = 'unexpected response length'
  1063. return -1
  1064. tag = unpack ( '>L', response[0:4] )[0]
  1065. return tag
  1066. def AssertInt32 ( value ):
  1067. assert(isinstance(value, (int, long)))
  1068. assert(value>=-2**32-1 and value<=2**32-1)
  1069. def AssertUInt32 ( value ):
  1070. assert(isinstance(value, (int, long)))
  1071. assert(value>=0 and value<=2**32-1)
  1072. def SetBit ( flag, bit, on ):
  1073. if on:
  1074. flag += ( 1<<bit )
  1075. else:
  1076. reset = 255 ^ ( 1<<bit )
  1077. flag = flag & reset
  1078. return flag
  1079. if sys.version_info > (3,):
  1080. def str_bytes(x):
  1081. return bytearray(x, 'utf-8')
  1082. else:
  1083. def str_bytes(x):
  1084. if isinstance(x,unicode):
  1085. return bytearray(x, 'utf-8')
  1086. else:
  1087. return bytearray(x)
  1088. def bytes_str(x):
  1089. assert (isinstance(x, bytearray))
  1090. return x.decode('utf-8')
  1091. #
  1092. # $Id$
  1093. #