SphinxClient.java 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255
  1. /*
  2. * $Id$
  3. *
  4. * Java version of Sphinx searchd client (Java API)
  5. *
  6. * Copyright (c) 2007-2008, Andrew Aksyonoff
  7. * Copyright (c) 2007, Vladimir Fedorkov
  8. * All rights reserved
  9. *
  10. * This program is free software; you can redistribute it and/or modify
  11. * it under the terms of the GNU General Public License. You should have
  12. * received a copy of the GPL license along with this program; if you
  13. * did not, you can find it at http://www.gnu.org/
  14. */
  15. package org.sphx.api;
  16. import java.io.*;
  17. import java.net.*;
  18. import java.util.*;
  19. /** Sphinx client class */
  20. public class SphinxClient
  21. {
  22. /* matching modes */
  23. public final static int SPH_MATCH_ALL = 0;
  24. public final static int SPH_MATCH_ANY = 1;
  25. public final static int SPH_MATCH_PHRASE = 2;
  26. public final static int SPH_MATCH_BOOLEAN = 3;
  27. public final static int SPH_MATCH_EXTENDED = 4;
  28. public final static int SPH_MATCH_FULLSCAN = 5;
  29. public final static int SPH_MATCH_EXTENDED2 = 6;
  30. /* ranking modes (extended2 only) */
  31. public final static int SPH_RANK_PROXIMITY_BM25 = 0;
  32. public final static int SPH_RANK_BM25 = 1;
  33. public final static int SPH_RANK_NONE = 2;
  34. public final static int SPH_RANK_WORDCOUNT = 3;
  35. /* sorting modes */
  36. public final static int SPH_SORT_RELEVANCE = 0;
  37. public final static int SPH_SORT_ATTR_DESC = 1;
  38. public final static int SPH_SORT_ATTR_ASC = 2;
  39. public final static int SPH_SORT_TIME_SEGMENTS = 3;
  40. public final static int SPH_SORT_EXTENDED = 4;
  41. public final static int SPH_SORT_EXPR = 5;
  42. /* grouping functions */
  43. public final static int SPH_GROUPBY_DAY = 0;
  44. public final static int SPH_GROUPBY_WEEK = 1;
  45. public final static int SPH_GROUPBY_MONTH = 2;
  46. public final static int SPH_GROUPBY_YEAR = 3;
  47. public final static int SPH_GROUPBY_ATTR = 4;
  48. public final static int SPH_GROUPBY_ATTRPAIR = 5;
  49. /* searchd reply status codes */
  50. public final static int SEARCHD_OK = 0;
  51. public final static int SEARCHD_ERROR = 1;
  52. public final static int SEARCHD_RETRY = 2;
  53. public final static int SEARCHD_WARNING = 3;
  54. /* attribute types */
  55. public final static int SPH_ATTR_INTEGER = 1;
  56. public final static int SPH_ATTR_TIMESTAMP = 2;
  57. public final static int SPH_ATTR_ORDINAL = 3;
  58. public final static int SPH_ATTR_BOOL = 4;
  59. public final static int SPH_ATTR_FLOAT = 5;
  60. public final static int SPH_ATTR_BIGINT = 6;
  61. public final static int SPH_ATTR_MULTI = 0x40000000;
  62. /* searchd commands */
  63. private final static int SEARCHD_COMMAND_SEARCH = 0;
  64. private final static int SEARCHD_COMMAND_EXCERPT = 1;
  65. private final static int SEARCHD_COMMAND_UPDATE = 2;
  66. private final static int SEARCHD_COMMAND_KEYWORDS = 3;
  67. /* searchd command versions */
  68. private final static int VER_MAJOR_PROTO = 0x1;
  69. private final static int VER_COMMAND_SEARCH = 0x116;
  70. private final static int VER_COMMAND_EXCERPT = 0x100;
  71. private final static int VER_COMMAND_UPDATE = 0x101;
  72. private final static int VER_COMMAND_KEYWORDS = 0x100;
  73. /* filter types */
  74. private final static int SPH_FILTER_VALUES = 0;
  75. private final static int SPH_FILTER_RANGE = 1;
  76. private final static int SPH_FILTER_FLOATRANGE = 2;
  77. private String _host;
  78. private int _port;
  79. private int _offset;
  80. private int _limit;
  81. private int _mode;
  82. private int[] _weights;
  83. private int _sort;
  84. private String _sortby;
  85. private int _minId;
  86. private int _maxId;
  87. private ByteArrayOutputStream _rawFilters;
  88. private DataOutputStream _filters;
  89. private int _filterCount;
  90. private String _groupBy;
  91. private int _groupFunc;
  92. private String _groupSort;
  93. private String _groupDistinct;
  94. private int _maxMatches;
  95. private int _cutoff;
  96. private int _retrycount;
  97. private int _retrydelay;
  98. private String _latitudeAttr;
  99. private String _longitudeAttr;
  100. private float _latitude;
  101. private float _longitude;
  102. private String _error;
  103. private String _warning;
  104. private ArrayList _reqs;
  105. private Map _indexWeights;
  106. private int _ranker;
  107. private int _maxQueryTime;
  108. private Map _fieldWeights;
  109. private Map _overrideTypes;
  110. private Map _overrideValues;
  111. private String _select;
  112. private static final int SPH_CLIENT_TIMEOUT_MILLISEC = 30000;
  113. /** Creates a new SphinxClient instance. */
  114. public SphinxClient()
  115. {
  116. this("localhost", 3312);
  117. }
  118. /** Creates a new SphinxClient instance, with host:port specification. */
  119. public SphinxClient(String host, int port)
  120. {
  121. _host = host;
  122. _port = port;
  123. _offset = 0;
  124. _limit = 20;
  125. _mode = SPH_MATCH_ALL;
  126. _sort = SPH_SORT_RELEVANCE;
  127. _sortby = "";
  128. _minId = 0;
  129. _maxId = 0;
  130. _filterCount = 0;
  131. _rawFilters = new ByteArrayOutputStream();
  132. _filters = new DataOutputStream(_rawFilters);
  133. _groupBy = "";
  134. _groupFunc = SPH_GROUPBY_DAY;
  135. _groupSort = "@group desc";
  136. _groupDistinct = "";
  137. _maxMatches = 1000;
  138. _cutoff = 0;
  139. _retrycount = 0;
  140. _retrydelay = 0;
  141. _latitudeAttr = null;
  142. _longitudeAttr = null;
  143. _latitude = 0;
  144. _longitude = 0;
  145. _error = "";
  146. _warning = "";
  147. _reqs = new ArrayList();
  148. _weights = null;
  149. _indexWeights = new LinkedHashMap();
  150. _fieldWeights = new LinkedHashMap();
  151. _ranker = SPH_RANK_PROXIMITY_BM25;
  152. _overrideTypes = new LinkedHashMap();
  153. _overrideValues = new LinkedHashMap();
  154. _select = "*";
  155. }
  156. /** Get last error message, if any. */
  157. public String GetLastError()
  158. {
  159. return _error;
  160. }
  161. /** Get last warning message, if any. */
  162. public String GetLastWarning()
  163. {
  164. return _warning;
  165. }
  166. /** Set searchd host and port to connect to. */
  167. public void SetServer(String host, int port) throws SphinxException
  168. {
  169. myAssert ( host!=null && host.length()>0, "host name must not be empty" );
  170. myAssert ( port>0 && port<65536, "port must be in 1..65535 range" );
  171. _host = host;
  172. _port = port;
  173. }
  174. /** Internal method. Sanity check. */
  175. private void myAssert ( boolean condition, String err ) throws SphinxException
  176. {
  177. if ( !condition )
  178. {
  179. _error = err;
  180. throw new SphinxException ( err );
  181. }
  182. }
  183. /** Internal method. String IO helper. */
  184. private static void writeNetUTF8 ( DataOutputStream ostream, String str ) throws IOException
  185. {
  186. if ( str==null )
  187. {
  188. ostream.writeInt ( 0 );
  189. return;
  190. }
  191. byte[] sBytes = str.getBytes ( "UTF-8" );
  192. int iLen = sBytes.length;
  193. ostream.writeInt ( iLen );
  194. ostream.write ( sBytes );
  195. }
  196. /** Internal method. String IO helper. */
  197. private static String readNetUTF8(DataInputStream istream) throws IOException
  198. {
  199. istream.readUnsignedShort (); /* searchd emits dword lengths, but Java expects words; lets just skip first 2 bytes */
  200. return istream.readUTF ();
  201. }
  202. /** Internal method. Unsigned int IO helper. */
  203. private static long readDword ( DataInputStream istream ) throws IOException
  204. {
  205. long v = (long) istream.readInt ();
  206. if ( v<0 )
  207. v += 4294967296L;
  208. return v;
  209. }
  210. /** Internal method. Connect to searchd and exchange versions. */
  211. private Socket _Connect()
  212. {
  213. Socket sock = null;
  214. try
  215. {
  216. sock = new Socket ( _host, _port );
  217. sock.setSoTimeout ( SPH_CLIENT_TIMEOUT_MILLISEC );
  218. DataInputStream sIn = new DataInputStream ( sock.getInputStream() );
  219. int version = sIn.readInt();
  220. if ( version<1 )
  221. {
  222. sock.close ();
  223. _error = "expected searchd protocol version 1+, got version " + version;
  224. return null;
  225. }
  226. DataOutputStream sOut = new DataOutputStream ( sock.getOutputStream() );
  227. sOut.writeInt ( VER_MAJOR_PROTO );
  228. } catch ( IOException e )
  229. {
  230. _error = "connection to " + _host + ":" + _port + " failed: " + e;
  231. try
  232. {
  233. if ( sock!=null )
  234. sock.close ();
  235. } catch ( IOException e1 ) {}
  236. return null;
  237. }
  238. return sock;
  239. }
  240. /** Internal method. Get and check response packet from searchd. */
  241. private byte[] _GetResponse ( Socket sock )
  242. {
  243. /* connect */
  244. DataInputStream sIn = null;
  245. InputStream SockInput = null;
  246. try
  247. {
  248. SockInput = sock.getInputStream();
  249. sIn = new DataInputStream ( SockInput );
  250. } catch ( IOException e )
  251. {
  252. _error = "getInputStream() failed: " + e;
  253. return null;
  254. }
  255. /* read response */
  256. byte[] response = null;
  257. short status = 0, ver = 0;
  258. int len = 0;
  259. try
  260. {
  261. /* read status fields */
  262. status = sIn.readShort();
  263. ver = sIn.readShort();
  264. len = sIn.readInt();
  265. /* read response if non-empty */
  266. if ( len<=0 )
  267. {
  268. _error = "invalid response packet size (len=" + len + ")";
  269. return null;
  270. }
  271. response = new byte[len];
  272. sIn.readFully ( response, 0, len );
  273. /* check status */
  274. if ( status==SEARCHD_WARNING )
  275. {
  276. DataInputStream in = new DataInputStream ( new ByteArrayInputStream ( response ) );
  277. int iWarnLen = in.readInt ();
  278. _warning = new String ( response, 4, iWarnLen );
  279. System.arraycopy ( response, 4+iWarnLen, response, 0, response.length-4-iWarnLen );
  280. } else if ( status==SEARCHD_ERROR )
  281. {
  282. _error = "searchd error: " + new String ( response, 4, response.length-4 );
  283. return null;
  284. } else if ( status==SEARCHD_RETRY )
  285. {
  286. _error = "temporary searchd error: " + new String ( response, 4, response.length-4 );
  287. return null;
  288. } else if ( status!=SEARCHD_OK )
  289. {
  290. _error = "searched returned unknown status, code=" + status;
  291. return null;
  292. }
  293. } catch ( IOException e )
  294. {
  295. if ( len!=0 )
  296. {
  297. /* get trace, to provide even more failure details */
  298. PrintWriter ew = new PrintWriter ( new StringWriter() );
  299. e.printStackTrace ( ew );
  300. ew.flush ();
  301. ew.close ();
  302. String sTrace = ew.toString ();
  303. /* build error message */
  304. _error = "failed to read searchd response (status=" + status + ", ver=" + ver + ", len=" + len + ", trace=" + sTrace +")";
  305. } else
  306. {
  307. _error = "received zero-sized searchd response (searchd crashed?): " + e.getMessage();
  308. }
  309. return null;
  310. } finally
  311. {
  312. try
  313. {
  314. if ( sIn!=null ) sIn.close();
  315. if ( sock!=null && !sock.isConnected() ) sock.close();
  316. } catch ( IOException e )
  317. {
  318. /* silently ignore close failures; nothing could be done anyway */
  319. }
  320. }
  321. return response;
  322. }
  323. /** Internal method. Connect to searchd, send request, get response as DataInputStream. */
  324. private DataInputStream _DoRequest ( int command, int version, ByteArrayOutputStream req )
  325. {
  326. /* connect */
  327. Socket sock = _Connect();
  328. if ( sock==null )
  329. return null;
  330. /* send request */
  331. byte[] reqBytes = req.toByteArray();
  332. try
  333. {
  334. DataOutputStream sockDS = new DataOutputStream ( sock.getOutputStream() );
  335. sockDS.writeShort ( command );
  336. sockDS.writeShort ( version );
  337. sockDS.writeInt ( reqBytes.length );
  338. sockDS.write ( reqBytes );
  339. } catch ( Exception e )
  340. {
  341. _error = "network error: " + e;
  342. return null;
  343. }
  344. /* get response */
  345. byte[] response = _GetResponse ( sock );
  346. if ( response==null )
  347. return null;
  348. /* spawn that tampon */
  349. return new DataInputStream ( new ByteArrayInputStream ( response ) );
  350. }
  351. /** Set matches offset and limit to return to client, max matches to retrieve on server, and cutoff. */
  352. public void SetLimits ( int offset, int limit, int max, int cutoff ) throws SphinxException
  353. {
  354. myAssert ( offset>=0, "offset must not be negative" );
  355. myAssert ( limit>0, "limit must be positive" );
  356. myAssert ( max>0, "max must be positive" );
  357. myAssert ( cutoff>=0, "cutoff must not be negative" );
  358. _offset = offset;
  359. _limit = limit;
  360. _maxMatches = max;
  361. _cutoff = cutoff;
  362. }
  363. /** Set matches offset and limit to return to client, and max matches to retrieve on server. */
  364. public void SetLimits ( int offset, int limit, int max ) throws SphinxException
  365. {
  366. SetLimits ( offset, limit, max, _cutoff );
  367. }
  368. /** Set matches offset and limit to return to client. */
  369. public void SetLimits ( int offset, int limit) throws SphinxException
  370. {
  371. SetLimits ( offset, limit, _maxMatches, _cutoff );
  372. }
  373. /** Set maximum query time, in milliseconds, per-index, 0 means "do not limit". */
  374. public void SetMaxQueryTime ( int maxTime ) throws SphinxException
  375. {
  376. myAssert ( maxTime>=0, "max_query_time must not be negative" );
  377. _maxQueryTime = maxTime;
  378. }
  379. /** Set matching mode. */
  380. public void SetMatchMode(int mode) throws SphinxException
  381. {
  382. myAssert (
  383. mode==SPH_MATCH_ALL ||
  384. mode==SPH_MATCH_ANY ||
  385. mode==SPH_MATCH_PHRASE ||
  386. mode==SPH_MATCH_BOOLEAN ||
  387. mode==SPH_MATCH_EXTENDED ||
  388. mode==SPH_MATCH_EXTENDED2, "unknown mode value; use one of the SPH_MATCH_xxx constants" );
  389. _mode = mode;
  390. }
  391. /** Set ranking mode. */
  392. public void SetRankingMode ( int ranker ) throws SphinxException
  393. {
  394. myAssert ( ranker==SPH_RANK_PROXIMITY_BM25
  395. || ranker==SPH_RANK_BM25
  396. || ranker==SPH_RANK_NONE
  397. || ranker==SPH_RANK_WORDCOUNT, "unknown ranker value; use one of the SPH_RANK_xxx constants" );
  398. _ranker = ranker;
  399. }
  400. /** Set sorting mode. */
  401. public void SetSortMode ( int mode, String sortby ) throws SphinxException
  402. {
  403. myAssert (
  404. mode==SPH_SORT_RELEVANCE ||
  405. mode==SPH_SORT_ATTR_DESC ||
  406. mode==SPH_SORT_ATTR_ASC ||
  407. mode==SPH_SORT_TIME_SEGMENTS ||
  408. mode==SPH_SORT_EXTENDED, "unknown mode value; use one of the available SPH_SORT_xxx constants" );
  409. myAssert ( mode==SPH_SORT_RELEVANCE || ( sortby!=null && sortby.length()>0 ), "sortby string must not be empty in selected mode" );
  410. _sort = mode;
  411. _sortby = ( sortby==null ) ? "" : sortby;
  412. }
  413. /** Set per-field weights (all values must be positive). WARNING: DEPRECATED, use SetFieldWeights() instead. */
  414. public void SetWeights(int[] weights) throws SphinxException
  415. {
  416. myAssert ( weights!=null, "weights must not be null" );
  417. for (int i = 0; i < weights.length; i++) {
  418. int weight = weights[i];
  419. myAssert ( weight>0, "all weights must be greater than 0" );
  420. }
  421. _weights = weights;
  422. }
  423. /**
  424. * Bind per-field weights by field name.
  425. * @param fieldWeights hash which maps String index names to Integer weights
  426. */
  427. public void SetFieldeights ( Map fieldWeights ) throws SphinxException
  428. {
  429. /* FIXME! implement checks here */
  430. _fieldWeights = ( fieldWeights==null ) ? new LinkedHashMap () : fieldWeights;
  431. }
  432. /**
  433. * Bind per-index weights by index name (and enable summing the weights on duplicate matches, instead of replacing them).
  434. * @param indexWeights hash which maps String index names to Integer weights
  435. */
  436. public void SetIndexWeights ( Map indexWeights ) throws SphinxException
  437. {
  438. /* FIXME! implement checks here */
  439. _indexWeights = ( indexWeights==null ) ? new LinkedHashMap () : indexWeights;
  440. }
  441. /** Set document IDs range to match. */
  442. public void SetIDRange ( int min, int max ) throws SphinxException
  443. {
  444. myAssert ( min<=max, "min must be less or equal to max" );
  445. _minId = min;
  446. _maxId = max;
  447. }
  448. /** Set values filter. Only match records where attribute value is in given set. */
  449. public void SetFilter ( String attribute, int[] values, boolean exclude ) throws SphinxException
  450. {
  451. myAssert ( values!=null && values.length>0, "values array must not be null or empty" );
  452. myAssert ( attribute!=null && attribute.length()>0, "attribute name must not be null or empty" );
  453. try
  454. {
  455. writeNetUTF8 ( _filters, attribute );
  456. _filters.writeInt ( SPH_FILTER_VALUES );
  457. _filters.writeInt ( values.length );
  458. for ( int i=0; i<values.length; i++ )
  459. _filters.writeLong ( values[i] );
  460. _filters.writeInt ( exclude ? 1 : 0 );
  461. } catch ( Exception e )
  462. {
  463. myAssert ( false, "IOException: " + e.getMessage() );
  464. }
  465. _filterCount++;
  466. }
  467. /** Set values filter. Only match records where attribute value is in given set. */
  468. public void SetFilter ( String attribute, long[] values, boolean exclude ) throws SphinxException
  469. {
  470. myAssert ( values!=null && values.length>0, "values array must not be null or empty" );
  471. myAssert ( attribute!=null && attribute.length()>0, "attribute name must not be null or empty" );
  472. try
  473. {
  474. writeNetUTF8 ( _filters, attribute );
  475. _filters.writeInt ( SPH_FILTER_VALUES );
  476. _filters.writeInt ( values.length );
  477. for ( int i=0; i<values.length; i++ )
  478. _filters.writeLong ( values[i] );
  479. _filters.writeInt ( exclude ? 1 : 0 );
  480. } catch ( Exception e )
  481. {
  482. myAssert ( false, "IOException: " + e.getMessage() );
  483. }
  484. _filterCount++;
  485. }
  486. /** Set values filter with a single value (syntax sugar; see {@link #SetFilter(String,int[],boolean)}). */
  487. public void SetFilter ( String attribute, int value, boolean exclude ) throws SphinxException
  488. {
  489. long[] values = new long[] { value };
  490. SetFilter ( attribute, values, exclude );
  491. }
  492. /** Set values filter with a single value (syntax sugar; see {@link #SetFilter(String,int[],boolean)}). */
  493. public void SetFilter ( String attribute, long value, boolean exclude ) throws SphinxException
  494. {
  495. long[] values = new long[] { value };
  496. SetFilter ( attribute, values, exclude );
  497. }
  498. /** Set integer range filter. Only match records if attribute value is beetwen min and max (inclusive). */
  499. public void SetFilterRange ( String attribute, long min, long max, boolean exclude ) throws SphinxException
  500. {
  501. myAssert ( min<=max, "min must be less or equal to max" );
  502. try
  503. {
  504. writeNetUTF8 ( _filters, attribute );
  505. _filters.writeInt ( SPH_FILTER_RANGE );
  506. _filters.writeLong ( min );
  507. _filters.writeLong ( max );
  508. _filters.writeInt ( exclude ? 1 : 0 );
  509. } catch ( Exception e )
  510. {
  511. myAssert ( false, "IOException: " + e.getMessage() );
  512. }
  513. _filterCount++;
  514. }
  515. /** Set integer range filter. Only match records if attribute value is beetwen min and max (inclusive). */
  516. public void SetFilterRange ( String attribute, int min, int max, boolean exclude ) throws SphinxException
  517. {
  518. SetFilterRange ( attribute, (long)min, (long)max, exclude );
  519. }
  520. /** Set float range filter. Only match records if attribute value is beetwen min and max (inclusive). */
  521. public void SetFilterFloatRange ( String attribute, float min, float max, boolean exclude ) throws SphinxException
  522. {
  523. myAssert ( min<=max, "min must be less or equal to max" );
  524. try
  525. {
  526. writeNetUTF8 ( _filters, attribute );
  527. _filters.writeInt ( SPH_FILTER_RANGE );
  528. _filters.writeFloat ( min );
  529. _filters.writeFloat ( max );
  530. _filters.writeInt ( exclude ? 1 : 0 );
  531. } catch ( Exception e )
  532. {
  533. myAssert ( false, "IOException: " + e.getMessage() );
  534. }
  535. _filterCount++;
  536. }
  537. /** Setup geographical anchor point. Required to use @geodist in filters and sorting; distance will be computed to this point. */
  538. public void SetGeoAnchor ( String latitudeAttr, String longitudeAttr, float latitude, float longitude ) throws SphinxException
  539. {
  540. myAssert ( latitudeAttr!=null && latitudeAttr.length()>0, "longitudeAttr string must not be null or empty" );
  541. myAssert ( longitudeAttr!=null && longitudeAttr.length()>0, "longitudeAttr string must not be null or empty" );
  542. _latitudeAttr = latitudeAttr;
  543. _longitudeAttr = longitudeAttr;
  544. _latitude = latitude;
  545. _longitude = longitude;
  546. }
  547. /** Set grouping attribute and function. */
  548. public void SetGroupBy ( String attribute, int func, String groupsort ) throws SphinxException
  549. {
  550. myAssert (
  551. func==SPH_GROUPBY_DAY ||
  552. func==SPH_GROUPBY_WEEK ||
  553. func==SPH_GROUPBY_MONTH ||
  554. func==SPH_GROUPBY_YEAR ||
  555. func==SPH_GROUPBY_ATTR ||
  556. func==SPH_GROUPBY_ATTRPAIR, "unknown func value; use one of the available SPH_GROUPBY_xxx constants" );
  557. _groupBy = attribute;
  558. _groupFunc = func;
  559. _groupSort = groupsort;
  560. }
  561. /** Set grouping attribute and function with default ("@group desc") groupsort (syntax sugar). */
  562. public void SetGroupBy(String attribute, int func) throws SphinxException
  563. {
  564. SetGroupBy(attribute, func, "@group desc");
  565. }
  566. /** Set count-distinct attribute for group-by queries. */
  567. public void SetGroupDistinct(String attribute)
  568. {
  569. _groupDistinct = attribute;
  570. }
  571. /** Set distributed retries count and delay. */
  572. public void SetRetries ( int count, int delay ) throws SphinxException
  573. {
  574. myAssert ( count>=0, "count must not be negative" );
  575. myAssert ( delay>=0, "delay must not be negative" );
  576. _retrycount = count;
  577. _retrydelay = delay;
  578. }
  579. /** Set distributed retries count with default (zero) delay (syntax sugar). */
  580. public void SetRetries ( int count ) throws SphinxException
  581. {
  582. SetRetries ( count, 0 );
  583. }
  584. /**
  585. * Set attribute values override (one override list per attribute).
  586. * @param values maps Long document IDs to Int/Long/Float values (as specified in attrtype).
  587. */
  588. public void SetOverride ( String attrname, int attrtype, Map values ) throws SphinxException
  589. {
  590. myAssert ( attrname!=null && attrname.length()>0, "attrname must not be empty" );
  591. myAssert ( attrtype==SPH_ATTR_INTEGER || attrtype==SPH_ATTR_TIMESTAMP || attrtype==SPH_ATTR_BOOL || attrtype==SPH_ATTR_FLOAT || attrtype==SPH_ATTR_BIGINT,
  592. "unsupported attrtype (must be one of INTEGER, TIMESTAMP, BOOL, FLOAT, or BIGINT)" );
  593. _overrideTypes.put ( attrname, new Integer ( attrtype ) );
  594. _overrideValues.put ( attrname, values );
  595. }
  596. /** Set select-list (attributes or expressions), SQL-like syntax. */
  597. public void SetSelect ( String select ) throws SphinxException
  598. {
  599. myAssert ( select!=null, "select clause string must not be null" );
  600. _select = select;
  601. }
  602. /** Reset all currently set filters (for multi-queries). */
  603. public void ResetFilters()
  604. {
  605. /* should we close them first? */
  606. _rawFilters = new ByteArrayOutputStream();
  607. _filters = new DataOutputStream(_rawFilters);
  608. _filterCount = 0;
  609. /* reset GEO anchor */
  610. _latitudeAttr = null;
  611. _longitudeAttr = null;
  612. _latitude = 0;
  613. _longitude = 0;
  614. }
  615. /** Clear groupby settings (for multi-queries). */
  616. public void ResetGroupBy ()
  617. {
  618. _groupBy = "";
  619. _groupFunc = SPH_GROUPBY_DAY;
  620. _groupSort = "@group desc";
  621. _groupDistinct = "";
  622. }
  623. /** Clear all attribute value overrides (for multi-queries). */
  624. public void ResetOverrides ()
  625. {
  626. _overrideTypes.clear ();
  627. _overrideValues.clear ();
  628. }
  629. /** Connect to searchd server and run current search query against all indexes (syntax sugar). */
  630. public SphinxResult Query ( String query ) throws SphinxException
  631. {
  632. return Query ( query, "*", "" );
  633. }
  634. /** Connect to searchd server and run current search query against all indexes (syntax sugar). */
  635. public SphinxResult Query ( String query, String index ) throws SphinxException
  636. {
  637. return Query ( query, index, "" );
  638. }
  639. /** Connect to searchd server and run current search query. */
  640. public SphinxResult Query ( String query, String index, String comment ) throws SphinxException
  641. {
  642. myAssert ( _reqs==null || _reqs.size()==0, "AddQuery() and Query() can not be combined; use RunQueries() instead" );
  643. AddQuery ( query, index, comment );
  644. SphinxResult[] results = RunQueries();
  645. if (results == null || results.length < 1) {
  646. return null; /* probably network error; error message should be already filled */
  647. }
  648. SphinxResult res = results[0];
  649. _warning = res.warning;
  650. _error = res.error;
  651. if (res == null || res.getStatus() == SEARCHD_ERROR) {
  652. return null;
  653. } else {
  654. return res;
  655. }
  656. }
  657. /** Add new query with current settings to current search request. */
  658. public int AddQuery ( String query, String index, String comment ) throws SphinxException
  659. {
  660. ByteArrayOutputStream req = new ByteArrayOutputStream();
  661. /* build request */
  662. try {
  663. DataOutputStream out = new DataOutputStream(req);
  664. out.writeInt(_offset);
  665. out.writeInt(_limit);
  666. out.writeInt(_mode);
  667. out.writeInt(_ranker);
  668. out.writeInt(_sort);
  669. writeNetUTF8(out, _sortby);
  670. writeNetUTF8(out, query);
  671. int weightLen = _weights != null ? _weights.length : 0;
  672. out.writeInt(weightLen);
  673. if (_weights != null) {
  674. for (int i = 0; i < _weights.length; i++)
  675. out.writeInt(_weights[i]);
  676. }
  677. writeNetUTF8(out, index);
  678. out.writeInt(0);
  679. out.writeInt(_minId);
  680. out.writeInt(_maxId);
  681. /* filters */
  682. out.writeInt(_filterCount);
  683. out.write(_rawFilters.toByteArray());
  684. /* group-by, max matches, sort-by-group flag */
  685. out.writeInt(_groupFunc);
  686. writeNetUTF8(out, _groupBy);
  687. out.writeInt(_maxMatches);
  688. writeNetUTF8(out, _groupSort);
  689. out.writeInt(_cutoff);
  690. out.writeInt(_retrycount);
  691. out.writeInt(_retrydelay);
  692. writeNetUTF8(out, _groupDistinct);
  693. /* anchor point */
  694. if (_latitudeAttr == null || _latitudeAttr.length() == 0 || _longitudeAttr == null || _longitudeAttr.length() == 0) {
  695. out.writeInt(0);
  696. } else {
  697. out.writeInt(1);
  698. writeNetUTF8(out, _latitudeAttr);
  699. writeNetUTF8(out, _longitudeAttr);
  700. out.writeFloat(_latitude);
  701. out.writeFloat(_longitude);
  702. }
  703. /* per-index weights */
  704. out.writeInt(_indexWeights.size());
  705. for (Iterator e = _indexWeights.keySet().iterator(); e.hasNext();) {
  706. String indexName = (String) e.next();
  707. Integer weight = (Integer) _indexWeights.get(indexName);
  708. writeNetUTF8(out, indexName);
  709. out.writeInt(weight.intValue());
  710. }
  711. /* max query time */
  712. out.writeInt ( _maxQueryTime );
  713. /* per-field weights */
  714. out.writeInt ( _fieldWeights.size() );
  715. for ( Iterator e=_fieldWeights.keySet().iterator(); e.hasNext(); )
  716. {
  717. String field = (String) e.next();
  718. Integer weight = (Integer) _fieldWeights.get ( field );
  719. writeNetUTF8 ( out, field );
  720. out.writeInt ( weight.intValue() );
  721. }
  722. /* comment */
  723. writeNetUTF8 ( out, comment );
  724. /* overrides */
  725. out.writeInt ( _overrideTypes.size() );
  726. for ( Iterator e=_overrideTypes.keySet().iterator(); e.hasNext(); )
  727. {
  728. String attr = (String) e.next();
  729. Integer type = (Integer) _overrideTypes.get ( attr );
  730. Map values = (Map) _overrideValues.get ( attr );
  731. writeNetUTF8 ( out, attr );
  732. out.writeInt ( type.intValue() );
  733. out.writeInt ( values.size() );
  734. for ( Iterator e2=values.keySet().iterator(); e2.hasNext(); )
  735. {
  736. Long id = (Long) e2.next ();
  737. out.writeLong ( id.longValue() );
  738. switch ( type.intValue() )
  739. {
  740. case SPH_ATTR_FLOAT: out.writeFloat ( ( (Float) values.get ( id ) ).floatValue() ); break;
  741. case SPH_ATTR_BIGINT: out.writeLong ( ( (Long)values.get ( id ) ).longValue() ); break;
  742. default: out.writeInt ( ( (Integer)values.get ( id ) ).intValue() ); break;
  743. }
  744. }
  745. }
  746. /* select-list */
  747. writeNetUTF8 ( out, _select );
  748. /* done! */
  749. out.flush ();
  750. int qIndex = _reqs.size();
  751. _reqs.add ( qIndex, req.toByteArray() );
  752. return qIndex;
  753. } catch ( Exception e )
  754. {
  755. myAssert ( false, "error in AddQuery(): " + e + ": " + e.getMessage() );
  756. } finally
  757. {
  758. try
  759. {
  760. _filters.close ();
  761. _rawFilters.close ();
  762. } catch ( IOException e )
  763. {
  764. myAssert ( false, "error in AddQuery(): " + e + ": " + e.getMessage() );
  765. }
  766. }
  767. return -1;
  768. }
  769. /** Run all previously added search queries. */
  770. public SphinxResult[] RunQueries() throws SphinxException
  771. {
  772. if ( _reqs==null || _reqs.size()<1 )
  773. {
  774. _error = "no queries defined, issue AddQuery() first";
  775. return null;
  776. }
  777. /* build the mega-request */
  778. int nreqs = _reqs.size();
  779. ByteArrayOutputStream reqBuf = new ByteArrayOutputStream();
  780. try
  781. {
  782. DataOutputStream req = new DataOutputStream ( reqBuf );
  783. req.writeInt ( nreqs );
  784. for ( int i=0; i<nreqs; i++ )
  785. req.write ( (byte[]) _reqs.get(i) );
  786. req.flush ();
  787. } catch ( Exception e )
  788. {
  789. _error = "internal error: failed to build request: " + e;
  790. return null;
  791. }
  792. DataInputStream in =_DoRequest ( SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, reqBuf );
  793. if ( in==null )
  794. return null;
  795. SphinxResult[] results = new SphinxResult [ nreqs ];
  796. _reqs = new ArrayList();
  797. try
  798. {
  799. for ( int ires=0; ires<nreqs; ires++ )
  800. {
  801. SphinxResult res = new SphinxResult();
  802. results[ires] = res;
  803. int status = in.readInt();
  804. res.setStatus ( status );
  805. if (status != SEARCHD_OK) {
  806. String message = readNetUTF8(in);
  807. if (status == SEARCHD_WARNING) {
  808. res.warning = message;
  809. } else {
  810. res.error = message;
  811. continue;
  812. }
  813. }
  814. /* read fields */
  815. int nfields = in.readInt();
  816. res.fields = new String[nfields];
  817. int pos = 0;
  818. for (int i = 0; i < nfields; i++)
  819. res.fields[i] = readNetUTF8(in);
  820. /* read arrts */
  821. int nattrs = in.readInt();
  822. res.attrTypes = new int[nattrs];
  823. res.attrNames = new String[nattrs];
  824. for (int i = 0; i < nattrs; i++) {
  825. String AttrName = readNetUTF8(in);
  826. int AttrType = in.readInt();
  827. res.attrNames[i] = AttrName;
  828. res.attrTypes[i] = AttrType;
  829. }
  830. /* read match count */
  831. int count = in.readInt();
  832. int id64 = in.readInt();
  833. res.matches = new SphinxMatch[count];
  834. for ( int matchesNo=0; matchesNo<count; matchesNo++ )
  835. {
  836. SphinxMatch docInfo;
  837. docInfo = new SphinxMatch (
  838. ( id64==0 ) ? readDword(in) : in.readLong(),
  839. in.readInt() );
  840. /* read matches */
  841. for (int attrNumber = 0; attrNumber < res.attrTypes.length; attrNumber++)
  842. {
  843. String attrName = res.attrNames[attrNumber];
  844. int type = res.attrTypes[attrNumber];
  845. /* handle bigints */
  846. if ( type==SPH_ATTR_BIGINT )
  847. {
  848. docInfo.attrValues.add ( attrNumber, new Long ( in.readLong() ) );
  849. continue;
  850. }
  851. /* handle floats */
  852. if ( type==SPH_ATTR_FLOAT )
  853. {
  854. docInfo.attrValues.add ( attrNumber, new Float ( in.readFloat() ) );
  855. continue;
  856. }
  857. /* handle everything else as unsigned ints */
  858. long val = readDword ( in );
  859. if ( ( type & SPH_ATTR_MULTI )!=0 )
  860. {
  861. long[] vals = new long [ (int)val ];
  862. for ( int k=0; k<val; k++ )
  863. vals[k] = readDword ( in );
  864. docInfo.attrValues.add ( attrNumber, vals );
  865. } else
  866. {
  867. docInfo.attrValues.add ( attrNumber, new Long ( val ) );
  868. }
  869. }
  870. res.matches[matchesNo] = docInfo;
  871. }
  872. res.total = in.readInt();
  873. res.totalFound = in.readInt();
  874. res.time = in.readInt() / 1000.0f;
  875. res.words = new SphinxWordInfo [ in.readInt() ];
  876. for ( int i=0; i<res.words.length; i++ )
  877. res.words[i] = new SphinxWordInfo ( readNetUTF8(in), readDword(in), readDword(in) );
  878. }
  879. return results;
  880. } catch ( IOException e )
  881. {
  882. _error = "incomplete reply";
  883. return null;
  884. }
  885. }
  886. /**
  887. * Connect to searchd server and generate excerpts (snippets) from given documents.
  888. * @param opts maps String keys to String or Integer values (see the documentation for complete keys list).
  889. * @return null on failure, array of snippets on success.
  890. */
  891. public String[] BuildExcerpts ( String[] docs, String index, String words, Map opts ) throws SphinxException
  892. {
  893. myAssert(docs != null && docs.length > 0, "BuildExcerpts: Have no documents to process");
  894. myAssert(index != null && index.length() > 0, "BuildExcerpts: Have no index to process documents");
  895. myAssert(words != null && words.length() > 0, "BuildExcerpts: Have no words to highlight");
  896. if (opts == null) opts = new LinkedHashMap();
  897. /* fixup options */
  898. if (!opts.containsKey("before_match")) opts.put("before_match", "<b>");
  899. if (!opts.containsKey("after_match")) opts.put("after_match", "</b>");
  900. if (!opts.containsKey("chunk_separator")) opts.put("chunk_separator", "...");
  901. if (!opts.containsKey("limit")) opts.put("limit", new Integer(256));
  902. if (!opts.containsKey("around")) opts.put("around", new Integer(5));
  903. if (!opts.containsKey("exact_phrase")) opts.put("exact_phrase", new Integer(0));
  904. if (!opts.containsKey("single_passage")) opts.put("single_passage", new Integer(0));
  905. if (!opts.containsKey("use_boundaries")) opts.put("use_boundaries", new Integer(0));
  906. if (!opts.containsKey("weight_order")) opts.put("weight_order", new Integer(0));
  907. /* build request */
  908. ByteArrayOutputStream reqBuf = new ByteArrayOutputStream();
  909. DataOutputStream req = new DataOutputStream ( reqBuf );
  910. try
  911. {
  912. req.writeInt(0);
  913. int iFlags = 1; /* remove_spaces */
  914. if ( ((Integer)opts.get("exact_phrase")).intValue()!=0 ) iFlags |= 2;
  915. if ( ((Integer)opts.get("single_passage")).intValue()!=0 ) iFlags |= 4;
  916. if ( ((Integer)opts.get("use_boundaries")).intValue()!=0 ) iFlags |= 8;
  917. if ( ((Integer)opts.get("weight_order")).intValue()!=0 ) iFlags |= 16;
  918. req.writeInt ( iFlags );
  919. writeNetUTF8 ( req, index );
  920. writeNetUTF8 ( req, words );
  921. /* send options */
  922. writeNetUTF8 ( req, (String) opts.get("before_match") );
  923. writeNetUTF8 ( req, (String) opts.get("after_match") );
  924. writeNetUTF8 ( req, (String) opts.get("chunk_separator") );
  925. req.writeInt ( ((Integer) opts.get("limit")).intValue() );
  926. req.writeInt ( ((Integer) opts.get("around")).intValue() );
  927. /* send documents */
  928. req.writeInt ( docs.length );
  929. for ( int i=0; i<docs.length; i++ )
  930. writeNetUTF8 ( req, docs[i] );
  931. req.flush();
  932. } catch ( Exception e )
  933. {
  934. _error = "internal error: failed to build request: " + e;
  935. return null;
  936. }
  937. DataInputStream in = _DoRequest ( SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, reqBuf );
  938. if ( in==null )
  939. return null;
  940. try
  941. {
  942. String[] res = new String [ docs.length ];
  943. for ( int i=0; i<docs.length; i++ )
  944. res[i] = readNetUTF8 ( in );
  945. return res;
  946. } catch ( Exception e )
  947. {
  948. _error = "incomplete reply";
  949. return null;
  950. }
  951. }
  952. /**
  953. * Connect to searchd server and update given attributes on given documents in given indexes.
  954. * Sample code that will set group_id=123 where id=1 and group_id=456 where id=3:
  955. *
  956. * <pre>
  957. * String[] attrs = new String[1];
  958. *
  959. * attrs[0] = "group_id";
  960. * long[][] values = new long[2][2];
  961. *
  962. * values[0] = new long[2]; values[0][0] = 1; values[0][1] = 123;
  963. * values[1] = new long[2]; values[1][0] = 3; values[1][1] = 456;
  964. *
  965. * int res = cl.UpdateAttributes ( "test1", attrs, values );
  966. * </pre>
  967. *
  968. * @param index index name(s) to update; might be distributed
  969. * @param attrs array with the names of the attributes to update
  970. * @param values array of updates; each long[] entry must contains document ID
  971. * in the first element, and all new attribute values in the following ones
  972. * @return -1 on failure, amount of actually found and updated documents (might be 0) on success
  973. *
  974. * @throws SphinxException on invalid parameters
  975. */
  976. public int UpdateAttributes ( String index, String[] attrs, long[][] values ) throws SphinxException
  977. {
  978. /* check args */
  979. myAssert ( index!=null && index.length()>0, "no index name provided" );
  980. myAssert ( attrs!=null && attrs.length>0, "no attribute names provided" );
  981. myAssert ( values!=null && values.length>0, "no update entries provided" );
  982. for ( int i=0; i<values.length; i++ )
  983. {
  984. myAssert ( values[i]!=null, "update entry #" + i + " is null" );
  985. myAssert ( values[i].length==1+attrs.length, "update entry #" + i + " has wrong length" );
  986. }
  987. /* build and send request */
  988. ByteArrayOutputStream reqBuf = new ByteArrayOutputStream();
  989. DataOutputStream req = new DataOutputStream ( reqBuf );
  990. try
  991. {
  992. writeNetUTF8 ( req, index );
  993. req.writeInt ( attrs.length );
  994. for ( int i=0; i<attrs.length; i++ )
  995. writeNetUTF8 ( req, attrs[i] );
  996. req.writeInt ( values.length );
  997. for ( int i=0; i<values.length; i++ )
  998. {
  999. req.writeLong ( values[i][0] ); /* send docid as 64bit value */
  1000. for ( int j=1; j<values[i].length; j++ )
  1001. req.writeInt ( (int)values[i][j] ); /* send values as 32bit values; FIXME! what happens when they are over 2^31? */
  1002. }
  1003. req.flush();
  1004. } catch ( Exception e )
  1005. {
  1006. _error = "internal error: failed to build request: " + e;
  1007. return -1;
  1008. }
  1009. /* get and parse response */
  1010. DataInputStream in = _DoRequest ( SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, reqBuf );
  1011. if ( in==null )
  1012. return -1;
  1013. try
  1014. {
  1015. return in.readInt ();
  1016. } catch ( Exception e )
  1017. {
  1018. _error = "incomplete reply";
  1019. return -1;
  1020. }
  1021. }
  1022. /**
  1023. * Connect to searchd server, and generate keyword list for a given query.
  1024. * Returns null on failure, an array of Maps with misc per-keyword info on success.
  1025. */
  1026. public Map[] BuildKeywords ( String query, String index, boolean hits ) throws SphinxException
  1027. {
  1028. /* build request */
  1029. ByteArrayOutputStream reqBuf = new ByteArrayOutputStream();
  1030. DataOutputStream req = new DataOutputStream ( reqBuf );
  1031. try
  1032. {
  1033. writeNetUTF8 ( req, query );
  1034. writeNetUTF8 ( req, index );
  1035. req.writeInt ( hits ? 1 : 0 );
  1036. } catch ( Exception e )
  1037. {
  1038. _error = "internal error: failed to build request: " + e;
  1039. return null;
  1040. }
  1041. /* run request */
  1042. DataInputStream in = _DoRequest ( SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, reqBuf );
  1043. if ( in==null )
  1044. return null;
  1045. /* parse reply */
  1046. try
  1047. {
  1048. int iNumWords = in.readInt ();
  1049. Map[] res = new Map[iNumWords];
  1050. for ( int i=0; i<iNumWords; i++ )
  1051. {
  1052. res[i] = new LinkedHashMap ();
  1053. res[i].put ( "tokenized", readNetUTF8 ( in ) );
  1054. res[i].put ( "normalized", readNetUTF8 ( in ) );
  1055. if ( hits )
  1056. {
  1057. res[i].put ( "docs", new Long ( readDword ( in ) ) );
  1058. res[i].put ( "hits", new Long ( readDword ( in ) ) );
  1059. }
  1060. }
  1061. return res;
  1062. } catch ( Exception e )
  1063. {
  1064. _error = "incomplete reply";
  1065. return null;
  1066. }
  1067. }
  1068. /** Escape the characters with special meaning in query syntax. */
  1069. public String EscapeString ( String s )
  1070. {
  1071. return s.replaceAll ( "[\\(\\)\\|\\-\\!\\@\\~\\\"\\&\\/]", "\\1" );
  1072. }
  1073. }
  1074. /*
  1075. * $Id$
  1076. */