SphinxClient.java 36 KB

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