SphinxClient.java 32 KB

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