SphinxClient.java 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  1. /*
  2. * $Id$
  3. *
  4. * Java version of Sphinx searchd client (Java API)
  5. *
  6. * Copyright (c) 2007, 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. /* sorting modes */
  29. public final static int SPH_SORT_RELEVANCE = 0;
  30. public final static int SPH_SORT_ATTR_DESC = 1;
  31. public final static int SPH_SORT_ATTR_ASC = 2;
  32. public final static int SPH_SORT_TIME_SEGMENTS = 3;
  33. public final static int SPH_SORT_EXTENDED = 4;
  34. /* grouping functions */
  35. public final static int SPH_GROUPBY_DAY = 0;
  36. public final static int SPH_GROUPBY_WEEK = 1;
  37. public final static int SPH_GROUPBY_MONTH = 2;
  38. public final static int SPH_GROUPBY_YEAR = 3;
  39. public final static int SPH_GROUPBY_ATTR = 4;
  40. public final static int SPH_GROUPBY_ATTRPAIR = 5;
  41. /* searchd reply status codes */
  42. public final static int SEARCHD_OK = 0;
  43. public final static int SEARCHD_ERROR = 1;
  44. public final static int SEARCHD_RETRY = 2;
  45. public final static int SEARCHD_WARNING = 3;
  46. /* attribute types */
  47. public final static int SPH_ATTR_INTEGER = 1;
  48. public final static int SPH_ATTR_TIMESTAMP = 2;
  49. public final static int SPH_ATTR_ORDINAL = 3;
  50. public final static int SPH_ATTR_BOOL = 4;
  51. public final static int SPH_ATTR_FLOAT = 5;
  52. public final static int SPH_ATTR_MULTI = 0x40000000;
  53. /* searchd commands */
  54. private final static int SEARCHD_COMMAND_SEARCH = 0;
  55. private final static int SEARCHD_COMMAND_EXCERPT= 1;
  56. private final static int SEARCHD_COMMAND_UPDATE = 2;
  57. /* searchd command versions */
  58. private final static int VER_MAJOR_PROTO = 0x1;
  59. private final static int VER_COMMAND_SEARCH = 0x10F;
  60. private final static int VER_COMMAND_EXCERPT = 0x100;
  61. private final static int VER_COMMAND_UPDATE = 0x100;
  62. /* filter types */
  63. private final static int SPH_FILTER_VALUES = 0;
  64. private final static int SPH_FILTER_RANGE = 1;
  65. private final static int SPH_FILTER_FLOATRANGE = 2;
  66. private String _host;
  67. private int _port;
  68. private int _offset;
  69. private int _limit;
  70. private int _mode;
  71. private int[] _weights;
  72. private int _sort;
  73. private String _sortby;
  74. private int _minId;
  75. private int _maxId;
  76. private ByteArrayOutputStream _rawFilters;
  77. private DataOutputStream _filters;
  78. private int _filterCount;
  79. private String _groupBy;
  80. private int _groupFunc;
  81. private String _groupSort;
  82. private String _groupDistinct;
  83. private int _maxMatches;
  84. private int _cutoff;
  85. private int _retrycount;
  86. private int _retrydelay;
  87. private String _latitudeAttr;
  88. private String _longitudeAttr;
  89. private float _latitude;
  90. private float _longitude;
  91. private String _error;
  92. private String _warning;
  93. private ArrayList _reqs;
  94. private Map _indexWeights;
  95. private static final int SPH_CLIENT_TIMEOUT_MILLISEC = 30000;
  96. /**
  97. * Creates new SphinxClient instance.
  98. *
  99. * Default host and port that the instance will connect to are
  100. * localhost:3312. That can be overriden using @see SetServer().
  101. */
  102. public SphinxClient()
  103. {
  104. this("localhost", 3312);
  105. }
  106. /**
  107. * Creates new SphinxClient instance, with host:port specification.
  108. *
  109. * Host and port can be later overriden using @see SetSever.
  110. *
  111. * @param host searchd host name (default: localhost)
  112. * @param port searchd port number (default: 3312)
  113. */
  114. public SphinxClient(String host, int port)
  115. {
  116. _host = host;
  117. _port = port;
  118. _offset = 0;
  119. _limit = 20;
  120. _mode = SPH_MATCH_ALL;
  121. _sort = SPH_SORT_RELEVANCE;
  122. _sortby = "";
  123. _minId = 0;
  124. _maxId = 0xFFFFFFFF;
  125. _filterCount = 0;
  126. _rawFilters = new ByteArrayOutputStream();
  127. _filters = new DataOutputStream(_rawFilters);
  128. _groupBy = "";
  129. _groupFunc = SPH_GROUPBY_DAY;
  130. _groupSort = "@group desc";
  131. _groupDistinct = "";
  132. _maxMatches = 1000;
  133. _cutoff = 0;
  134. _retrycount = 0;
  135. _retrydelay = 0;
  136. _latitudeAttr = null;
  137. _longitudeAttr = null;
  138. _latitude = 0;
  139. _longitude = 0;
  140. _error = "";
  141. _warning = "";
  142. _reqs = new ArrayList();
  143. _weights = null;
  144. _indexWeights = new LinkedHashMap();
  145. }
  146. /**
  147. * Get last error message, if any.
  148. *
  149. * @return string with last error message (empty string if no errors occured)
  150. */
  151. public String GetLastError()
  152. {
  153. return _error;
  154. }
  155. /**
  156. * Get last warning message, if any.
  157. *
  158. * @return string with last warning message (empty string if no errors occured)
  159. */
  160. public String GetLastWarning()
  161. {
  162. return _warning;
  163. }
  164. /**
  165. * Set searchd host and port to connect to.
  166. *
  167. * @param host searchd host name (default: localhost)
  168. * @param port searchd port number (default: 3312)
  169. *
  170. * @throws SphinxException on invalid parameters
  171. */
  172. public void SetServer(String host, int port) throws SphinxException
  173. {
  174. myAssert ( host!=null && host.length()>0, "host name must not be empty" );
  175. myAssert ( port>0 && port<65536, "port must be in 1..65535 range" );
  176. _host = host;
  177. _port = port;
  178. }
  179. /** Connect to searchd and exchange versions (internal method). */
  180. private Socket _Connect()
  181. {
  182. Socket sock;
  183. try {
  184. sock = new Socket(_host, _port);
  185. sock.setSoTimeout(SPH_CLIENT_TIMEOUT_MILLISEC);
  186. } catch (IOException e) {
  187. _error = "connection to " + _host + ":" + _port + " failed: " + e.getMessage();
  188. return null;
  189. }
  190. DataInputStream sIn;
  191. DataOutputStream sOut;
  192. try {
  193. InputStream sockInput = sock.getInputStream();
  194. OutputStream sockOutput = sock.getOutputStream();
  195. sIn = new DataInputStream(sockInput);
  196. int version = sIn.readInt();
  197. if (version < 1) {
  198. sock.close();
  199. _error = "expected searchd protocol version 1+, got version " + version;
  200. return null;
  201. }
  202. sOut = new DataOutputStream(sockOutput);
  203. sOut.writeInt(VER_MAJOR_PROTO);
  204. } catch (IOException e) {
  205. _error = "Connect: Read from socket failed: " + e.getMessage();
  206. try {
  207. sock.close();
  208. } catch (IOException e1) {
  209. _error = _error + " Cannot close socket: " + e1.getMessage();
  210. }
  211. return null;
  212. }
  213. return sock;
  214. }
  215. /** Get and check response packet from searchd (internal method). */
  216. private byte[] _GetResponse(Socket sock, int client_ver) throws SphinxException
  217. {
  218. short status = 0, ver = 0;
  219. int len = 0;
  220. byte[] response = new byte[65536];
  221. DataInputStream sIn = null;
  222. InputStream SockInput = null;
  223. try
  224. {
  225. SockInput = sock.getInputStream();
  226. sIn = new DataInputStream(SockInput);
  227. } catch (IOException e)
  228. {
  229. myAssert ( false, "getInputStream() failed: " + e.getMessage() );
  230. return null;
  231. }
  232. try {
  233. /* read status fields */
  234. status = sIn.readShort();
  235. ver = sIn.readShort();
  236. len = sIn.readInt();
  237. /* read response if non-empty */
  238. myAssert(len > 0, "zero-sized searchd response reported");
  239. if (len > 0) {
  240. response = new byte[len];
  241. sIn.readFully(response, 0, len);
  242. } else
  243. {
  244. /* FIXME! no response, return null? */
  245. }
  246. /* check status */
  247. /* FIXME! fix error string conversion */
  248. if (status == SEARCHD_WARNING) {
  249. int wlen = sIn.readInt();
  250. byte[] byteWarn = new byte[wlen];
  251. sIn.readFully(byteWarn, 0, wlen);
  252. String warning = new String(byteWarn);
  253. _warning = warning;
  254. return null;
  255. } else if (status == SEARCHD_ERROR) {
  256. _error = "searchd error: " + new String(response);
  257. return null;
  258. } else if (status == SEARCHD_RETRY) {
  259. _error = "temporary searchd error: " + new String(response);
  260. return null;
  261. }
  262. if (status != SEARCHD_OK) {
  263. _error = "unknown status code '" + status + "'";
  264. return null;
  265. }
  266. /* check version */
  267. if (ver < client_ver) {
  268. _warning = "searchd command v." + (ver >> 8) + "." + (ver & 0xff) + "older than client's v." + (client_ver >> 8) + "." + (client_ver & 0xff) + ", some options might not work";
  269. }
  270. } catch (IOException e) {
  271. if (len != 0) {
  272. StringBuilder error = new StringBuilder();
  273. error.append("failed to read searchd response (status=").append(status);
  274. error.append(", ver=").append(ver);
  275. error.append(", len=").append(len);
  276. error.append(") reason ");
  277. error.append(e.getMessage());
  278. error.append(" trace:\n ");
  279. /* get trace here to let user know which request failed */
  280. StringWriter errStream = new StringWriter();
  281. PrintWriter errWriter = new PrintWriter(errStream);
  282. e.printStackTrace(errWriter);
  283. errWriter.flush();
  284. errWriter.close();
  285. error.append(errWriter.toString());
  286. _error = error.toString();
  287. } else {
  288. _error = _error + " Received zero-sized searchd response " + e.getMessage();
  289. }
  290. return null;
  291. } finally {
  292. try {
  293. if (sIn != null) sIn.close();
  294. } catch (IOException e) {
  295. _error = _error + " Unable to close searchd response input stream: " + e.getMessage();
  296. }
  297. try {
  298. if (sock != null && !sock.isConnected()) sock.close();
  299. } catch (IOException e) {
  300. _error = _error + " Unable to close searchd socket: " + e.getMessage();
  301. }
  302. }
  303. return response;
  304. }
  305. /** Set matches offset and limit to return to client, max matches to retrieve on server, and cutoff. */
  306. public void SetLimits ( int offset, int limit, int max, int cutoff ) throws SphinxException
  307. {
  308. myAssert ( offset>=0, "offset must be greater than or equal to 0" );
  309. myAssert ( limit>0, "limit must be greater than 0" );
  310. myAssert ( max>0, "max must be greater than 0" );
  311. myAssert ( cutoff>=0, "max must be greater than or equal to 0" );
  312. _offset = offset;
  313. _limit = limit;
  314. _maxMatches = max;
  315. _cutoff = cutoff;
  316. }
  317. /** Set matches offset and limit to return to client, and max matches to retrieve on server. */
  318. public void SetLimits ( int offset, int limit, int max ) throws SphinxException
  319. {
  320. SetLimits ( offset, limit, max, 0 );
  321. }
  322. /** Set matches offset and limit to return to client. */
  323. public void SetLimits ( int offset, int limit) throws SphinxException
  324. {
  325. SetLimits ( offset, limit, 0, 0 );
  326. }
  327. /** Set matching mode. */
  328. public void SetMatchMode(int mode) throws SphinxException
  329. {
  330. myAssert (
  331. mode==SPH_MATCH_ALL ||
  332. mode==SPH_MATCH_ANY ||
  333. mode==SPH_MATCH_PHRASE ||
  334. mode==SPH_MATCH_BOOLEAN ||
  335. mode==SPH_MATCH_EXTENDED, "unknown mode value; use one of the available SPH_MATCH_xxx constants" );
  336. _mode = mode;
  337. }
  338. /** Set sorting mode. */
  339. public void SetSortMode ( int mode, String sortby ) throws SphinxException
  340. {
  341. myAssert (
  342. mode==SPH_SORT_RELEVANCE ||
  343. mode==SPH_SORT_ATTR_DESC ||
  344. mode==SPH_SORT_ATTR_ASC ||
  345. mode==SPH_SORT_TIME_SEGMENTS ||
  346. mode==SPH_SORT_EXTENDED, "unknown mode value; use one of the available SPH_SORT_xxx constants" );
  347. myAssert ( mode==SPH_SORT_RELEVANCE || ( sortby!=null && sortby.length()>0 ), "sortby string must not be empty in selected mode" );
  348. _sort = mode;
  349. _sortby = ( sortby==null ) ? "" : sortby;
  350. }
  351. /** Set per-field weights (all values must be positive). */
  352. public void SetWeights(int[] weights) throws SphinxException
  353. {
  354. myAssert ( weights!=null, "weights must not be null" );
  355. for (int i = 0; i < weights.length; i++) {
  356. int weight = weights[i];
  357. myAssert ( weight>0, "all weights must be greater than 0" );
  358. }
  359. _weights = weights;
  360. }
  361. /**
  362. * Set per-index weights
  363. *
  364. * @param indexWeights hash which maps String index names to Integer weights
  365. */
  366. public void SetIndexWeights ( Map indexWeights ) throws SphinxException
  367. {
  368. /* FIXME! implement checks here */
  369. _indexWeights = indexWeights;
  370. }
  371. /**
  372. * Set document IDs range to match.
  373. *
  374. * Only match those documents where document ID is beetwen given
  375. * min and max values (including themselves).
  376. *
  377. * @param min minimum document ID to match
  378. * @param max maximum document ID to match
  379. *
  380. * @throws SphinxException on invalid parameters
  381. */
  382. public void SetIDRange ( int min, int max ) throws SphinxException
  383. {
  384. myAssert ( min<=max, "min must be less or equal to max" );
  385. _minId = min;
  386. _maxId = max;
  387. }
  388. /**
  389. * Set values filter.
  390. *
  391. * Only match those documents where <code>attribute</code> column value
  392. * is in given values set.
  393. *
  394. * @param attribute attribute name to filter by
  395. * @param values values set to match the attribute value by
  396. * @param exclude whether to exclude matching documents instead
  397. *
  398. * @throws SphinxException on invalid parameters
  399. */
  400. public void SetFilter ( String attribute, int[] values, boolean exclude ) throws SphinxException
  401. {
  402. myAssert ( values!=null && values.length>0, "values array must not be null or empty" );
  403. myAssert ( attribute!=null && attribute.length()>0, "attribute name must not be null or empty" );
  404. try
  405. {
  406. writeNetUTF8 ( _filters, attribute );
  407. _filters.writeInt ( SPH_FILTER_VALUES );
  408. _filters.writeInt ( values.length );
  409. for ( int i=0; i<values.length; i++ )
  410. _filters.writeInt ( values[i] );
  411. _filters.writeInt ( exclude ? 1 : 0 );
  412. } catch ( Exception e )
  413. {
  414. myAssert ( false, "IOException: " + e.getMessage() );
  415. }
  416. _filterCount++;
  417. }
  418. /**
  419. * Set values filter with a single value (syntax sugar).
  420. *
  421. * @see SetFilter ( String attribute, int[] values, boolean exclude )
  422. */
  423. public void SetFilter ( String attribute, int value, boolean exclude ) throws SphinxException
  424. {
  425. int[] values = new int[] { value };
  426. SetFilter ( attribute, values, exclude );
  427. }
  428. /**
  429. * Set integer range filter.
  430. *
  431. * Only match those documents where <code>attribute</code> column value
  432. * is beetwen given min and max values (including themselves).
  433. *
  434. * @param attribute attribute name to filter by
  435. * @param min min attribute value
  436. * @param max max attribute value
  437. * @param exclude whether to exclude matching documents instead
  438. *
  439. * @throws SphinxException on invalid parameters
  440. */
  441. public void SetFilterRange ( String attribute, int min, int max, boolean exclude ) throws SphinxException
  442. {
  443. myAssert ( min<=max, "min must be less or equal to max" );
  444. try
  445. {
  446. writeNetUTF8 ( _filters, attribute );
  447. _filters.writeInt ( SPH_FILTER_RANGE );
  448. _filters.writeInt ( min );
  449. _filters.writeInt ( max );
  450. _filters.writeInt ( exclude ? 1 : 0 );
  451. } catch ( Exception e )
  452. {
  453. myAssert ( false, "IOException: " + e.getMessage() );
  454. }
  455. _filterCount++;
  456. }
  457. /**
  458. * Set float range filter.
  459. *
  460. * Only match those documents where <code>attribute</code> column value
  461. * is beetwen given min and max values (including themselves).
  462. *
  463. * @param attribute attribute name to filter by
  464. * @param min min attribute value
  465. * @param max max attribute value
  466. * @param exclude whether to exclude matching documents instead
  467. *
  468. * @throws SphinxException on invalid parameters
  469. * Set float range filter.
  470. */
  471. public void SetFilterFloatRange ( String attribute, float min, float max, boolean exclude ) throws SphinxException
  472. {
  473. myAssert ( min<=max, "min must be less or equal to max" );
  474. try
  475. {
  476. writeNetUTF8 ( _filters, attribute );
  477. _filters.writeInt ( SPH_FILTER_RANGE );
  478. _filters.writeFloat ( min );
  479. _filters.writeFloat ( max );
  480. _filters.writeInt ( exclude ? 1 : 0 );
  481. } catch ( Exception e )
  482. {
  483. myAssert ( false, "IOException: " + e.getMessage() );
  484. }
  485. _filterCount++;
  486. }
  487. /**
  488. * Setup geographical anchor point.
  489. *
  490. * Required to use @geodist in filters and sorting.
  491. * Distance will be computed to this point.
  492. *
  493. * @param latitudeAttr the name of latitude attribute
  494. * @param longitudeAttr the name of longitude attribute
  495. * @param latitude anchor point latitude, in radians
  496. * @param longitude anchor point longitude, in radians
  497. *
  498. * @throws SphinxException on invalid parameters
  499. */
  500. public void SetGeoAnchor ( String latitudeAttr, String longitudeAttr, float latitude, float longitude ) throws SphinxException
  501. {
  502. myAssert ( latitudeAttr!=null && latitudeAttr.length()>0, "longitudeAttr string must not be null or empty" );
  503. myAssert ( longitudeAttr!=null && longitudeAttr.length()>0, "longitudeAttr string must not be null or empty" );
  504. _latitudeAttr = latitudeAttr;
  505. _longitudeAttr = longitudeAttr;
  506. _latitude = latitude;
  507. _longitude = longitude;
  508. }
  509. /** Set grouping attribute and function. */
  510. public void SetGroupBy ( String attribute, int func, String groupsort ) throws SphinxException
  511. {
  512. myAssert (
  513. func==SPH_GROUPBY_DAY ||
  514. func==SPH_GROUPBY_WEEK ||
  515. func==SPH_GROUPBY_MONTH ||
  516. func==SPH_GROUPBY_YEAR ||
  517. func==SPH_GROUPBY_ATTR ||
  518. func==SPH_GROUPBY_ATTRPAIR, "unknown func value; use one of the available SPH_GROUPBY_xxx constants" );
  519. _groupBy = attribute;
  520. _groupFunc = func;
  521. _groupSort = groupsort;
  522. }
  523. /** Set grouping attribute and function with default ("@group desc") groupsort (syntax sugar). */
  524. public void SetGroupBy(String attribute, int func) throws SphinxException
  525. {
  526. SetGroupBy(attribute, func, "@group desc");
  527. }
  528. /** Set count-distinct attribute for group-by queries. */
  529. public void SetGroupDistinct(String attribute)
  530. {
  531. _groupDistinct = attribute;
  532. }
  533. /** Set distributed retries count and delay. */
  534. public void SetRetries ( int count, int delay ) throws SphinxException
  535. {
  536. myAssert ( count>=0, "count must not be negative" );
  537. myAssert ( delay>=0, "delay must not be negative" );
  538. _retrycount = count;
  539. _retrydelay = delay;
  540. }
  541. /** Set distributed retries count with default (zero) delay (syntax sugar). */
  542. public void SetRetries ( int count ) throws SphinxException
  543. {
  544. SetRetries ( count, 0 );
  545. }
  546. /** Reset all currently set filters (for multi-queries). */
  547. public void ResetFilters()
  548. {
  549. /* should we close them first? */
  550. _rawFilters = new ByteArrayOutputStream();
  551. _filters = new DataOutputStream(_rawFilters);
  552. _filterCount = 0;
  553. /* reset GEO anchor */
  554. _latitudeAttr = null;
  555. _longitudeAttr = null;
  556. _latitude = 0;
  557. _longitude = 0;
  558. }
  559. /** Connect to searchd server and run current search query against all indexes (syntax sugar). */
  560. public SphinxResult Query(String query) throws SphinxException
  561. {
  562. return Query(query, "*");
  563. }
  564. /**
  565. * Connect to searchd server and run current search query.
  566. *
  567. * @param query query string
  568. * @param index index name(s) to query. May contain anything-separated
  569. * list of index names, or "*" which means to query all indexes.
  570. * @return {@link SphinxResult} object
  571. *
  572. * @throws SphinxException on invalid parameters
  573. */
  574. public SphinxResult Query ( String query, String index ) throws SphinxException
  575. {
  576. myAssert ( _reqs==null || _reqs.size()==0, "AddQuery() and Query() can not be combined; use RunQueries() instead" );
  577. AddQuery(query, index);
  578. SphinxResult[] results = RunQueries();
  579. if (results == null || results.length < 1) {
  580. return null; /* probably network error; error message should be already filled */
  581. }
  582. SphinxResult res = results[0];
  583. _warning = res.warning;
  584. _error = res.error;
  585. if (res == null || res.getStatus() == SEARCHD_ERROR) {
  586. return null;
  587. } else {
  588. return res;
  589. }
  590. }
  591. /** Add new query with current settings to current search request. */
  592. public int AddQuery ( String query, String index ) throws SphinxException
  593. {
  594. ByteArrayOutputStream req = new ByteArrayOutputStream();
  595. /* build request */
  596. try {
  597. DataOutputStream out = new DataOutputStream(req);
  598. out.writeInt(_offset);
  599. out.writeInt(_limit);
  600. out.writeInt(_mode);
  601. out.writeInt(_sort);
  602. writeNetUTF8(out, _sortby);
  603. writeNetUTF8(out, query);
  604. int weightLen = _weights != null ? _weights.length : 0;
  605. out.writeInt(weightLen);
  606. if (_weights != null) {
  607. for (int i = 0; i < _weights.length; i++)
  608. out.writeInt(_weights[i]);
  609. }
  610. writeNetUTF8(out, index);
  611. out.writeInt(0);
  612. out.writeInt(_minId);
  613. out.writeInt(_maxId);
  614. /* filters */
  615. out.writeInt(_filterCount);
  616. out.write(_rawFilters.toByteArray());
  617. /* group-by, max matches, sort-by-group flag */
  618. out.writeInt(_groupFunc);
  619. writeNetUTF8(out, _groupBy);
  620. out.writeInt(_maxMatches);
  621. writeNetUTF8(out, _groupSort);
  622. out.writeInt(_cutoff);
  623. out.writeInt(_retrycount);
  624. out.writeInt(_retrydelay);
  625. writeNetUTF8(out, _groupDistinct);
  626. /* anchor point */
  627. if (_latitudeAttr == null || _latitudeAttr.length() == 0 || _longitudeAttr == null || _longitudeAttr.length() == 0) {
  628. out.writeInt(0);
  629. } else {
  630. out.writeInt(1);
  631. writeNetUTF8(out, _latitudeAttr);
  632. writeNetUTF8(out, _longitudeAttr);
  633. out.writeFloat(_latitude);
  634. out.writeFloat(_longitude);
  635. }
  636. /* per-index weights */
  637. out.writeInt(_indexWeights.size());
  638. for (Iterator e = _indexWeights.keySet().iterator(); e.hasNext();) {
  639. String indexName = (String) e.next();
  640. Integer weight = (Integer) _indexWeights.get(indexName);
  641. writeNetUTF8(out, indexName);
  642. out.writeInt(weight.intValue());
  643. }
  644. out.flush();
  645. int qIndex = _reqs.size();
  646. _reqs.add(qIndex, req.toByteArray());
  647. return qIndex;
  648. } catch (Exception ex) {
  649. myAssert(false, "error on AddQuery: " + ex.getMessage());
  650. } finally {
  651. try {
  652. _filters.close();
  653. _rawFilters.close();
  654. } catch (IOException e) {
  655. myAssert(false, "error on AddQuery: " + e.getMessage());
  656. }
  657. }
  658. return -1;
  659. }
  660. /** Run all previously added search queries. */
  661. public SphinxResult[] RunQueries() throws SphinxException
  662. {
  663. if (_reqs == null || _reqs.size() < 1) {
  664. _error = "no queries defined, issue AddQuery() first";
  665. return null;
  666. }
  667. Socket sock = _Connect();
  668. if (sock == null) return null;
  669. /* send query, get response */
  670. ByteArrayOutputStream req = new ByteArrayOutputStream();
  671. DataOutputStream prepareRQ = null;
  672. int nreqs = _reqs.size();
  673. try {
  674. prepareRQ = new DataOutputStream(req);
  675. prepareRQ.writeShort(SEARCHD_COMMAND_SEARCH);
  676. prepareRQ.writeShort(VER_COMMAND_SEARCH);
  677. int rqLen = 4;
  678. for (int i = 0; i < nreqs; i++) {
  679. byte[] subRq = (byte[]) _reqs.get(i);
  680. rqLen += subRq.length;
  681. }
  682. prepareRQ.writeInt(rqLen);
  683. prepareRQ.writeInt(nreqs);
  684. for (int i = 0; i < nreqs; i++) {
  685. byte[] subRq = (byte[]) _reqs.get(i);
  686. prepareRQ.write(subRq);
  687. }
  688. OutputStream SockOut = sock.getOutputStream();
  689. byte[] reqBytes = req.toByteArray();
  690. SockOut.write(reqBytes);
  691. } catch (Exception e) {
  692. myAssert(false, "Query: Unable to create read/write streams: " + e.getMessage());
  693. return null;
  694. }
  695. /* reset requests */
  696. _reqs = new ArrayList();
  697. /* get response */
  698. byte[] response = null;
  699. response = _GetResponse(sock, VER_COMMAND_SEARCH);
  700. if (response == null) return null;
  701. /* parse response */
  702. SphinxResult[] results = new SphinxResult[nreqs];
  703. DataInputStream in;
  704. in = new DataInputStream(new ByteArrayInputStream(response));
  705. /* read schema */
  706. int ires;
  707. try {
  708. for (ires = 0; ires < nreqs; ires++) {
  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. SphinxMatch docInfo;
  744. if (id64 != 0) {
  745. int docHi = in.readInt();
  746. int docLo = in.readInt();
  747. long doc64 = docHi;
  748. doc64 = doc64 << 32 + docLo;
  749. int weight = in.readInt();
  750. docInfo = new SphinxMatch(doc64, weight);
  751. } else {
  752. int docId = in.readInt();
  753. int weight = in.readInt();
  754. docInfo = new SphinxMatch(docId, weight);
  755. }
  756. /* read matches */
  757. for (int attrNumber = 0; attrNumber < res.attrTypes.length; attrNumber++)
  758. {
  759. String attrName = res.attrNames[attrNumber];
  760. int type = res.attrTypes[attrNumber];
  761. /* handle floats */
  762. if ( type==SPH_ATTR_FLOAT )
  763. {
  764. docInfo.attrValues.add ( attrNumber, new Float ( in.readFloat() ) );
  765. continue;
  766. }
  767. /* handle everything else as unsigned ints */
  768. int val = in.readInt();
  769. if ( ( type & SPH_ATTR_MULTI )!=0 )
  770. {
  771. int[] vals = new int[val];
  772. for ( int k=0; k<val; k++ )
  773. vals[k] = in.readInt();
  774. docInfo.attrValues.add ( attrNumber, vals );
  775. } else
  776. {
  777. docInfo.attrValues.add ( attrNumber, val );
  778. }
  779. }
  780. res.matches[matchesNo] = docInfo;
  781. }
  782. res.total = in.readInt();
  783. res.totalFound = in.readInt();
  784. res.time = in.readInt() / 1000; /* FIXME! format should be %.3f */
  785. int wordCount = in.readInt();
  786. res.words = new SphinxWordInfo[wordCount];
  787. for (int i = 0; i < wordCount; i++) {
  788. String word = readNetUTF8(in);
  789. int docs = in.readInt();
  790. int hits = in.readInt();
  791. SphinxWordInfo winfo = new SphinxWordInfo(word, docs, hits);
  792. res.words[i] = winfo;
  793. }
  794. }
  795. in.close();
  796. return results;
  797. } catch (IOException e) {
  798. myAssert(false, "Query: Unable to parse response: " + e.getMessage());
  799. } finally {
  800. try {
  801. in.close();
  802. } catch (IOException e) {
  803. myAssert(false, "Query: Unable to close datain stream : " + e.getMessage());
  804. }
  805. }
  806. return null;
  807. }
  808. /**
  809. * Connect to searchd server and generate exceprts from given documents.
  810. *
  811. * @param docs an array of strings which represent the documents' contents
  812. * @param index a string with the name of the index which settings will be used for stemming, lexing and case folding
  813. * @param words a string which contains the query words to highlight
  814. * @param opts a hash with additional optional highlighting parameters:
  815. * <ul>
  816. * <li><b>"before_match"</b>, a string to insert before a set of matching words (default is "&lt;b*gt;");
  817. * <li><b>"after_match"</b>, a string to insert after a set of matching words (default is "&lt;/b*gt;");
  818. * <li><b>"chunk_separator"</b>, a string to insert between excerpts chunks (default is "...");
  819. * <li><b>"limit"</b>, max excerpt size in codepoints (default is 256);
  820. * <li><b>"around"</b>, how much words to highlight around each match (default is 5).
  821. * @return null on failure, an array of string excerpts on success
  822. *
  823. * @throws SphinxException on invalid parameters
  824. */
  825. public String[] BuildExcerpts ( String[] docs, String index, String words, Map opts ) throws SphinxException
  826. {
  827. myAssert(docs != null && docs.length > 0, "BuildExcerpts: Have no documents to process");
  828. myAssert(index != null && index.length() > 0, "BuildExcerpts: Have no index to process documents");
  829. myAssert(words != null && words.length() > 0, "BuildExcerpts: Have no words to highlight");
  830. if (opts == null) opts = new LinkedHashMap();
  831. Socket sock = _Connect();
  832. if (sock == null) return null;
  833. /* fixup options */
  834. if (!opts.containsKey("before_match")) opts.put("before_match", "<b>");
  835. if (!opts.containsKey("after_match")) opts.put("after_match", "</b>");
  836. if (!opts.containsKey("chunk_separator")) opts.put("chunk_separator", "...");
  837. if (!opts.containsKey("limit")) opts.put("limit", new Integer(256));
  838. if (!opts.containsKey("around")) opts.put("around", new Integer(5));
  839. /* build request */
  840. ByteArrayOutputStream req = new ByteArrayOutputStream();
  841. DataOutputStream rqData = null;
  842. DataOutputStream socketDS = null;
  843. try {
  844. rqData = new DataOutputStream(req);
  845. /* v.1.0 req */
  846. rqData.writeInt(0);
  847. rqData.writeInt(1);
  848. writeNetUTF8(rqData, index);
  849. writeNetUTF8(rqData, words);
  850. /* send options */
  851. writeNetUTF8(rqData, (String) opts.get("before_match"));
  852. writeNetUTF8(rqData, (String) opts.get("after_match"));
  853. writeNetUTF8(rqData, (String) opts.get("chunk_separator"));
  854. rqData.writeInt(((Integer) opts.get("limit")).intValue());
  855. rqData.writeInt(((Integer) opts.get("around")).intValue());
  856. /* send documents */
  857. for (int i = 0; i < docs.length; i++) {
  858. myAssert(docs[i] != null, "BuildExcerpts: empty document #" + i);
  859. writeNetUTF8(rqData, docs[i]);
  860. }
  861. rqData.flush();
  862. byte[] byteRq = req.toByteArray();
  863. /* send query, get response */
  864. OutputStream SockOut = sock.getOutputStream();
  865. socketDS = new DataOutputStream(SockOut);
  866. socketDS.writeShort(SEARCHD_COMMAND_EXCERPT);
  867. socketDS.writeShort(VER_COMMAND_EXCERPT);
  868. socketDS.writeInt(byteRq.length + 8);
  869. socketDS.write(byteRq);
  870. } catch (Exception ex) {
  871. myAssert(false, "BuildExcerpts: Unable to create read/write streams: " + ex.getMessage());
  872. return null;
  873. }
  874. try {
  875. /* get response */
  876. byte[] response = null;
  877. String[] docsXrpt = new String[docs.length];
  878. response = _GetResponse(sock, VER_COMMAND_SEARCH);
  879. if (response == null) return null;
  880. /* parse response */
  881. DataInputStream in;
  882. in = new DataInputStream(new ByteArrayInputStream(response));
  883. for (int i = 0; i < docs.length; i++) {
  884. docsXrpt[i] = readNetUTF8(in);
  885. }
  886. return docsXrpt;
  887. } catch (Exception e) {
  888. myAssert(false, "BuildExcerpts: incomplete response " + e.getMessage());
  889. return null;
  890. }
  891. }
  892. /** Internal sanity check. */
  893. private void myAssert ( boolean condition, String err ) throws SphinxException
  894. {
  895. if ( !condition )
  896. {
  897. _error = err;
  898. throw new SphinxException ( err );
  899. }
  900. }
  901. /** String IO helper (internal method). */
  902. private static void writeNetUTF8(DataOutputStream ostream, String str) throws IOException
  903. {
  904. ostream.writeShort(0);
  905. ostream.writeUTF(str);
  906. }
  907. /** String IO helper (internal method). */
  908. private static String readNetUTF8(DataInputStream istream) throws IOException
  909. {
  910. istream.readUnsignedShort (); /* searchd emits dword lengths, but Java expects words; lets just skip first 2 bytes */
  911. return istream.readUTF ();
  912. }
  913. }
  914. /*
  915. * $Id$
  916. */