Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
13815 anikendra 1
<?php
2
/**
3
 * Copyright (c) 2007-2011, Servigistics, Inc.
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  - Redistributions of source code must retain the above copyright notice,
10
 *    this list of conditions and the following disclaimer.
11
 *  - Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *  - Neither the name of Servigistics, Inc. nor the names of
15
 *    its contributors may be used to endorse or promote products derived from
16
 *    this software without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
 * POSSIBILITY OF SUCH DAMAGE.
29
 *
30
 * @copyright Copyright 2007-2011 Servigistics, Inc. (http://servigistics.com)
31
 * @license http://solr-php-client.googlecode.com/svn/trunk/COPYING New BSD
32
 * @version $Id: Service.php 59 2011-02-08 20:38:59Z donovan.jimenez $
33
 *
34
 * @package Apache
35
 * @subpackage Solr
36
 * @author Donovan Jimenez <djimenez@conduit-it.com>
37
 */
38
 
39
// See Issue #1 (http://code.google.com/p/solr-php-client/issues/detail?id=1)
40
// Doesn't follow typical include path conventions, but is more convenient for users
41
require_once(dirname(__FILE__) . '/Exception.php');
42
require_once(dirname(__FILE__) . '/HttpTransportException.php');
43
require_once(dirname(__FILE__) . '/InvalidArgumentException.php');
44
 
45
require_once(dirname(__FILE__) . '/Document.php');
46
require_once(dirname(__FILE__) . '/Response.php');
47
 
48
require_once(dirname(__FILE__) . '/HttpTransport/Interface.php');
49
 
50
/**
51
 * Starting point for the Solr API. Represents a Solr server resource and has
52
 * methods for pinging, adding, deleting, committing, optimizing and searching.
53
 *
54
 * Example Usage:
55
 * <code>
56
 * ...
57
 * $solr = new Apache_Solr_Service(); //or explicitly new Apache_Solr_Service('localhost', 8180, '/solr')
58
 *
59
 * if ($solr->ping())
60
 * {
61
 * 		$solr->deleteByQuery('*:*'); //deletes ALL documents - be careful :)
62
 *
63
 * 		$document = new Apache_Solr_Document();
64
 * 		$document->id = uniqid(); //or something else suitably unique
65
 *
66
 * 		$document->title = 'Some Title';
67
 * 		$document->content = 'Some content for this wonderful document. Blah blah blah.';
68
 *
69
 * 		$solr->addDocument($document); 	//if you're going to be adding documents in bulk using addDocuments
70
 * 										//with an array of documents is faster
71
 *
72
 * 		$solr->commit(); //commit to see the deletes and the document
73
 * 		$solr->optimize(); //merges multiple segments into one
74
 *
75
 * 		//and the one we all care about, search!
76
 * 		//any other common or custom parameters to the request handler can go in the
77
 * 		//optional 4th array argument.
78
 * 		$solr->search('content:blah', 0, 10, array('sort' => 'timestamp desc'));
79
 * }
80
 * ...
81
 * </code>
82
 *
83
 * @todo Investigate using other HTTP clients other than file_get_contents built-in handler. Could provide performance
84
 * improvements when dealing with multiple requests by using HTTP's keep alive functionality
85
 */
86
class Apache_Solr_Service
87
{
88
	/**
89
	 * SVN Revision meta data for this class
90
	 */
91
	const SVN_REVISION = '$Revision: 59 $';
92
 
93
	/**
94
	 * SVN ID meta data for this class
95
	 */
96
	const SVN_ID = '$Id: Service.php 59 2011-02-08 20:38:59Z donovan.jimenez $';
97
 
98
	/**
99
	 * Response writer we'll request - JSON. See http://code.google.com/p/solr-php-client/issues/detail?id=6#c1 for reasoning
100
	 */
101
	const SOLR_WRITER = 'json';
102
 
103
	/**
104
	 * NamedList Treatment constants
105
	 */
106
	const NAMED_LIST_FLAT = 'flat';
107
	const NAMED_LIST_MAP = 'map';
108
 
109
	/**
110
	 * Search HTTP Methods
111
	 */
112
	const METHOD_GET = 'GET';
113
	const METHOD_POST = 'POST';
114
 
115
	/**
116
	 * Servlet mappings
117
	 */
118
	const PING_SERVLET = 'admin/ping';
119
	const UPDATE_SERVLET = 'update';
120
	const SEARCH_SERVLET = 'select';
121
	const THREADS_SERVLET = 'admin/threads';
122
	const EXTRACT_SERVLET = 'update/extract';
123
 
124
	/**
125
	 * Server identification strings
126
	 *
127
	 * @var string
128
	 */
129
	protected $_host, $_port, $_path;
130
 
131
	/**
132
	 * Whether {@link Apache_Solr_Response} objects should create {@link Apache_Solr_Document}s in
133
	 * the returned parsed data
134
	 *
135
	 * @var boolean
136
	 */
137
	protected $_createDocuments = true;
138
 
139
	/**
140
	 * Whether {@link Apache_Solr_Response} objects should have multivalue fields with only a single value
141
	 * collapsed to appear as a single value would.
142
	 *
143
	 * @var boolean
144
	 */
145
	protected $_collapseSingleValueArrays = true;
146
 
147
	/**
148
	 * How NamedLists should be formatted in the output.  This specifically effects facet counts. Valid values
149
	 * are {@link Apache_Solr_Service::NAMED_LIST_MAP} (default) or {@link Apache_Solr_Service::NAMED_LIST_FLAT}.
150
	 *
151
	 * @var string
152
	 */
153
	protected $_namedListTreatment = self::NAMED_LIST_MAP;
154
 
155
	/**
156
	 * Query delimiters. Someone might want to be able to change
157
	 * these (to use &amp; instead of & for example), so I've provided them.
158
	 *
159
	 * @var string
160
	 */
161
	protected $_queryDelimiter = '?', $_queryStringDelimiter = '&', $_queryBracketsEscaped = true;
162
 
163
	/**
164
	 * Constructed servlet full path URLs
165
	 *
166
	 * @var string
167
	 */
168
	protected $_pingUrl, $_updateUrl, $_searchUrl, $_threadsUrl;
169
 
170
	/**
171
	 * Keep track of whether our URLs have been constructed
172
	 *
173
	 * @var boolean
174
	 */
175
	protected $_urlsInited = false;
176
 
177
	/**
178
	 * HTTP Transport implementation (pluggable)
179
	 *
180
	 * @var Apache_Solr_HttpTransport_Interface
181
	 */
182
	protected $_httpTransport = false;
183
 
184
	/**
185
	 * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc.
186
	 *
187
	 * NOTE: inside a phrase fewer characters need escaped, use {@link Apache_Solr_Service::escapePhrase()} instead
188
	 *
189
	 * @param string $value
190
	 * @return string
191
	 */
192
	static public function escape($value)
193
	{
194
		//list taken from http://lucene.apache.org/java/docs/queryparsersyntax.html#Escaping%20Special%20Characters
195
		$pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/';
196
		$replace = '\\\$1';
197
 
198
		return preg_replace($pattern, $replace, $value);
199
	}
200
 
201
	/**
202
	 * Escape a value meant to be contained in a phrase for special query characters
203
	 *
204
	 * @param string $value
205
	 * @return string
206
	 */
207
	static public function escapePhrase($value)
208
	{
209
		$pattern = '/("|\\\)/';
210
		$replace = '\\\$1';
211
 
212
		return preg_replace($pattern, $replace, $value);
213
	}
214
 
215
	/**
216
	 * Convenience function for creating phrase syntax from a value
217
	 *
218
	 * @param string $value
219
	 * @return string
220
	 */
221
	static public function phrase($value)
222
	{
223
		return '"' . self::escapePhrase($value) . '"';
224
	}
225
 
226
	/**
227
	 * Constructor. All parameters are optional and will take on default values
228
	 * if not specified.
229
	 *
230
	 * @param string $host
231
	 * @param string $port
232
	 * @param string $path
233
	 * @param Apache_Solr_HttpTransport_Interface $httpTransport
234
	 */
235
	public function __construct($host = 'localhost', $port = 8180, $path = '/solr/', $httpTransport = false)
236
	{
237
		$this->setHost($host);
238
		$this->setPort($port);
239
		$this->setPath($path);
240
 
241
		$this->_initUrls();
242
 
243
		if ($httpTransport)
244
		{
245
			$this->setHttpTransport($httpTransport);
246
		}
247
 
248
		// check that our php version is >= 5.1.3 so we can correct for http_build_query behavior later
249
		$this->_queryBracketsEscaped = version_compare(phpversion(), '5.1.3', '>=');
250
	}
251
 
252
	/**
253
	 * Return a valid http URL given this server's host, port and path and a provided servlet name
254
	 *
255
	 * @param string $servlet
256
	 * @return string
257
	 */
258
	protected function _constructUrl($servlet, $params = array())
259
	{
260
		if (count($params))
261
		{
262
			//escape all parameters appropriately for inclusion in the query string
263
			$escapedParams = array();
264
 
265
			foreach ($params as $key => $value)
266
			{
267
				$escapedParams[] = urlencode($key) . '=' . urlencode($value);
268
			}
269
 
270
			$queryString = $this->_queryDelimiter . implode($this->_queryStringDelimiter, $escapedParams);
271
		}
272
		else
273
		{
274
			$queryString = '';
275
		}
276
 
277
		return 'http://' . $this->_host . ':' . $this->_port . $this->_path . $servlet . $queryString;
278
	}
279
 
280
	/**
281
	 * Construct the Full URLs for the three servlets we reference
282
	 */
283
	protected function _initUrls()
284
	{
285
		//Initialize our full servlet URLs now that we have server information
286
		$this->_extractUrl = $this->_constructUrl(self::EXTRACT_SERVLET);
287
		$this->_pingUrl = $this->_constructUrl(self::PING_SERVLET);
288
		$this->_searchUrl = $this->_constructUrl(self::SEARCH_SERVLET);
289
		$this->_threadsUrl = $this->_constructUrl(self::THREADS_SERVLET, array('wt' => self::SOLR_WRITER ));
290
		$this->_updateUrl = $this->_constructUrl(self::UPDATE_SERVLET, array('wt' => self::SOLR_WRITER ));
291
 
292
		$this->_urlsInited = true;
293
	}
294
 
295
	protected function _generateQueryString($params)
296
	{
297
		// use http_build_query to encode our arguments because its faster
298
		// than urlencoding all the parts ourselves in a loop
299
		//
300
		// because http_build_query treats arrays differently than we want to, correct the query
301
		// string by changing foo[#]=bar (# being an actual number) parameter strings to just
302
		// multiple foo=bar strings. This regex should always work since '=' will be urlencoded
303
		// anywhere else the regex isn't expecting it
304
		//
305
		// NOTE: before php 5.1.3 brackets were not url encoded by http_build query - we've checked
306
		// the php version in the constructor and put the results in the instance variable. Also, before
307
		// 5.1.2 the arg_separator parameter was not available, so don't use it
308
		if ($this->_queryBracketsEscaped)
309
		{
310
			$queryString = http_build_query($params, null, $this->_queryStringDelimiter);
311
			return preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', $queryString);
312
		}
313
		else
314
		{
315
			$queryString = http_build_query($params);
316
			return preg_replace('/\\[(?:[0-9]|[1-9][0-9]+)\\]=/', '=', $queryString);
317
		}
318
	}
319
 
320
	/**
321
	 * Central method for making a get operation against this Solr Server
322
	 *
323
	 * @param string $url
324
	 * @param float $timeout Read timeout in seconds
325
	 * @return Apache_Solr_Response
326
	 *
327
	 * @throws Apache_Solr_HttpTransportException If a non 200 response status is returned
328
	 */
329
	protected function _sendRawGet($url, $timeout = FALSE)
330
	{
331
		$httpTransport = $this->getHttpTransport();
332
 
333
		$httpResponse = $httpTransport->performGetRequest($url, $timeout);
334
		$solrResponse = new Apache_Solr_Response($httpResponse, $this->_createDocuments, $this->_collapseSingleValueArrays);
335
 
336
		if ($solrResponse->getHttpStatus() != 200)
337
		{
338
			throw new Apache_Solr_HttpTransportException($solrResponse);
339
		}
340
 
341
		return $solrResponse;
342
	}
343
 
344
	/**
345
	 * Central method for making a post operation against this Solr Server
346
	 *
347
	 * @param string $url
348
	 * @param string $rawPost
349
	 * @param float $timeout Read timeout in seconds
350
	 * @param string $contentType
351
	 * @return Apache_Solr_Response
352
	 *
353
	 * @throws Apache_Solr_HttpTransportException If a non 200 response status is returned
354
	 */
355
	protected function _sendRawPost($url, $rawPost, $timeout = FALSE, $contentType = 'text/xml; charset=UTF-8')
356
	{
357
		$httpTransport = $this->getHttpTransport();
358
 
359
		$httpResponse = $httpTransport->performPostRequest($url, $rawPost, $contentType, $timeout);
360
		$solrResponse = new Apache_Solr_Response($httpResponse, $this->_createDocuments, $this->_collapseSingleValueArrays);
361
 
362
		if ($solrResponse->getHttpStatus() != 200)
363
		{
364
			throw new Apache_Solr_HttpTransportException($solrResponse);
365
		}
366
 
367
		return $solrResponse;
368
	}
369
 
370
	/**
371
	 * Returns the set host
372
	 *
373
	 * @return string
374
	 */
375
	public function getHost()
376
	{
377
		return $this->_host;
378
	}
379
 
380
	/**
381
	 * Set the host used. If empty will fallback to constants
382
	 *
383
	 * @param string $host
384
	 *
385
	 * @throws Apache_Solr_InvalidArgumentException If the host parameter is empty
386
	 */
387
	public function setHost($host)
388
	{
389
		//Use the provided host or use the default
390
		if (empty($host))
391
		{
392
			throw new Apache_Solr_InvalidArgumentException('Host parameter is empty');
393
		}
394
		else
395
		{
396
			$this->_host = $host;
397
		}
398
 
399
		if ($this->_urlsInited)
400
		{
401
			$this->_initUrls();
402
		}
403
	}
404
 
405
	/**
406
	 * Get the set port
407
	 *
408
	 * @return integer
409
	 */
410
	public function getPort()
411
	{
412
		return $this->_port;
413
	}
414
 
415
	/**
416
	 * Set the port used. If empty will fallback to constants
417
	 *
418
	 * @param integer $port
419
	 *
420
	 * @throws Apache_Solr_InvalidArgumentException If the port parameter is empty
421
	 */
422
	public function setPort($port)
423
	{
424
		//Use the provided port or use the default
425
		$port = (int) $port;
426
 
427
		if ($port <= 0)
428
		{
429
			throw new Apache_Solr_InvalidArgumentException('Port is not a valid port number');
430
		}
431
		else
432
		{
433
			$this->_port = $port;
434
		}
435
 
436
		if ($this->_urlsInited)
437
		{
438
			$this->_initUrls();
439
		}
440
	}
441
 
442
	/**
443
	 * Get the set path.
444
	 *
445
	 * @return string
446
	 */
447
	public function getPath()
448
	{
449
		return $this->_path;
450
	}
451
 
452
	/**
453
	 * Set the path used. If empty will fallback to constants
454
	 *
455
	 * @param string $path
456
	 */
457
	public function setPath($path)
458
	{
459
		$path = trim($path, '/');
460
 
461
		$this->_path = '/' . $path . '/';
462
 
463
		if ($this->_urlsInited)
464
		{
465
			$this->_initUrls();
466
		}
467
	}
468
 
469
	/**
470
	 * Get the current configured HTTP Transport
471
	 *
472
	 * @return HttpTransportInterface
473
	 */
474
	public function getHttpTransport()
475
	{
476
		// lazy load a default if one has not be set
477
		if ($this->_httpTransport === false)
478
		{
479
			require_once(dirname(__FILE__) . '/HttpTransport/FileGetContents.php');
480
 
481
			$this->_httpTransport = new Apache_Solr_HttpTransport_FileGetContents();
482
		}
483
 
484
		return $this->_httpTransport;
485
	}
486
 
487
	/**
488
	 * Set the HTTP Transport implemenation that will be used for all HTTP requests
489
	 *
490
	 * @param Apache_Solr_HttpTransport_Interface
491
	 */
492
	public function setHttpTransport(Apache_Solr_HttpTransport_Interface $httpTransport)
493
	{
494
		$this->_httpTransport = $httpTransport;
495
	}
496
 
497
	/**
498
	 * Set the create documents flag. This determines whether {@link Apache_Solr_Response} objects will
499
	 * parse the response and create {@link Apache_Solr_Document} instances in place.
500
	 *
501
	 * @param boolean $createDocuments
502
	 */
503
	public function setCreateDocuments($createDocuments)
504
	{
505
		$this->_createDocuments = (bool) $createDocuments;
506
	}
507
 
508
	/**
509
	 * Get the current state of teh create documents flag.
510
	 *
511
	 * @return boolean
512
	 */
513
	public function getCreateDocuments()
514
	{
515
		return $this->_createDocuments;
516
	}
517
 
518
	/**
519
	 * Set the collapse single value arrays flag.
520
	 *
521
	 * @param boolean $collapseSingleValueArrays
522
	 */
523
	public function setCollapseSingleValueArrays($collapseSingleValueArrays)
524
	{
525
		$this->_collapseSingleValueArrays = (bool) $collapseSingleValueArrays;
526
	}
527
 
528
	/**
529
	 * Get the current state of the collapse single value arrays flag.
530
	 *
531
	 * @return boolean
532
	 */
533
	public function getCollapseSingleValueArrays()
534
	{
535
		return $this->_collapseSingleValueArrays;
536
	}
537
 
538
	/**
539
	 * Get the current default timeout setting (initially the default_socket_timeout ini setting)
540
	 * in seconds
541
	 *
542
	 * @return float
543
	 *
544
	 * @deprecated Use the getDefaultTimeout method on the HTTP transport implementation
545
	 */
546
	public function getDefaultTimeout()
547
	{
548
		return $this->getHttpTransport()->getDefaultTimeout();
549
	}
550
 
551
	/**
552
	 * Set the default timeout for all calls that aren't passed a specific timeout
553
	 *
554
	 * @param float $timeout Timeout value in seconds
555
	 *
556
	 * @deprecated Use the setDefaultTimeout method on the HTTP transport implementation
557
	 */
558
	public function setDefaultTimeout($timeout)
559
	{
560
		$this->getHttpTransport()->setDefaultTimeout($timeout);
561
	}
562
 
563
	/**
564
	 * Set how NamedLists should be formatted in the response data. This mainly effects
565
	 * the facet counts format.
566
	 *
567
	 * @param string $namedListTreatment
568
	 * @throws Apache_Solr_InvalidArgumentException If invalid option is set
569
	 */
570
	public function setNamedListTreatment($namedListTreatment)
571
	{
572
		switch ((string) $namedListTreatment)
573
		{
574
			case Apache_Solr_Service::NAMED_LIST_FLAT:
575
				$this->_namedListTreatment = Apache_Solr_Service::NAMED_LIST_FLAT;
576
				break;
577
 
578
			case Apache_Solr_Service::NAMED_LIST_MAP:
579
				$this->_namedListTreatment = Apache_Solr_Service::NAMED_LIST_MAP;
580
				break;
581
 
582
			default:
583
				throw new Apache_Solr_InvalidArgumentException('Not a valid named list treatement option');
584
		}
585
	}
586
 
587
	/**
588
	 * Get the current setting for named list treatment.
589
	 *
590
	 * @return string
591
	 */
592
	public function getNamedListTreatment()
593
	{
594
		return $this->_namedListTreatment;
595
	}
596
 
597
	/**
598
	 * Set the string used to separate the path form the query string.
599
	 * Defaulted to '?'
600
	 *
601
	 * @param string $queryDelimiter
602
	 */
603
	public function setQueryDelimiter($queryDelimiter)
604
	{
605
		$this->_queryDelimiter = $queryDelimiter;
606
	}
607
 
608
	/**
609
	 * Set the string used to separate the parameters in thequery string
610
	 * Defaulted to '&'
611
	 *
612
	 * @param string $queryStringDelimiter
613
	 */
614
	public function setQueryStringDelimiter($queryStringDelimiter)
615
	{
616
		$this->_queryStringDelimiter = $queryStringDelimiter;
617
	}
618
 
619
	/**
620
	 * Call the /admin/ping servlet, can be used to quickly tell if a connection to the
621
	 * server is able to be made.
622
	 *
623
	 * @param float $timeout maximum time to wait for ping in seconds, -1 for unlimited (default is 2)
624
	 * @return float Actual time taken to ping the server, FALSE if timeout or HTTP error status occurs
625
	 */
626
	public function ping($timeout = 2)
627
	{
628
		$start = microtime(true);
629
 
630
		$httpTransport = $this->getHttpTransport();
631
 
632
		$httpResponse = $httpTransport->performHeadRequest($this->_pingUrl, $timeout);
633
		$solrResponse = new Apache_Solr_Response($httpResponse, $this->_createDocuments, $this->_collapseSingleValueArrays);
634
 
635
		if ($solrResponse->getHttpStatus() == 200)
636
		{
637
			return microtime(true) - $start;
638
		}
639
		else
640
		{
641
			return false;
642
		}
643
	}
644
 
645
	/**
646
	 * Call the /admin/threads servlet and retrieve information about all threads in the
647
	 * Solr servlet's thread group. Useful for diagnostics.
648
	 *
649
	 * @return Apache_Solr_Response
650
	 *
651
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
652
	 */
653
	public function threads()
654
	{
655
		return $this->_sendRawGet($this->_threadsUrl);
656
	}
657
 
658
	/**
659
	 * Raw Add Method. Takes a raw post body and sends it to the update service.  Post body
660
	 * should be a complete and well formed "add" xml document.
661
	 *
662
	 * @param string $rawPost
663
	 * @return Apache_Solr_Response
664
	 *
665
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
666
	 */
667
	public function add($rawPost)
668
	{
669
		return $this->_sendRawPost($this->_updateUrl, $rawPost);
670
	}
671
 
672
	/**
673
	 * Add a Solr Document to the index
674
	 *
675
	 * @param Apache_Solr_Document $document
676
	 * @param boolean $allowDups
677
	 * @param boolean $overwritePending
678
	 * @param boolean $overwriteCommitted
679
	 * @param integer $commitWithin The number of milliseconds that a document must be committed within, see @{link http://wiki.apache.org/solr/UpdateXmlMessages#The_Update_Schema} for details.  If left empty this property will not be set in the request.
680
	 * @return Apache_Solr_Response
681
	 *
682
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
683
	 */
684
	public function addDocument(Apache_Solr_Document $document, $allowDups = false, $overwritePending = true, $overwriteCommitted = true, $commitWithin = 0)
685
	{
686
		$dupValue = $allowDups ? 'true' : 'false';
687
		$pendingValue = $overwritePending ? 'true' : 'false';
688
		$committedValue = $overwriteCommitted ? 'true' : 'false';
689
 
690
		$commitWithin = (int) $commitWithin;
691
		$commitWithinString = $commitWithin > 0 ? " commitWithin=\"{$commitWithin}\"" : '';
692
 
693
		$rawPost = "<add allowDups=\"{$dupValue}\" overwritePending=\"{$pendingValue}\" overwriteCommitted=\"{$committedValue}\"{$commitWithinString}>";
694
		$rawPost .= $this->_documentToXmlFragment($document);
695
		$rawPost .= '</add>';
696
 
697
		return $this->add($rawPost);
698
	}
699
 
700
	/**
701
	 * Add an array of Solr Documents to the index all at once
702
	 *
703
	 * @param array $documents Should be an array of Apache_Solr_Document instances
704
	 * @param boolean $allowDups
705
	 * @param boolean $overwritePending
706
	 * @param boolean $overwriteCommitted
707
	 * @param integer $commitWithin The number of milliseconds that a document must be committed within, see @{link http://wiki.apache.org/solr/UpdateXmlMessages#The_Update_Schema} for details.  If left empty this property will not be set in the request.
708
	 * @return Apache_Solr_Response
709
	 *
710
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
711
	 */
712
	public function addDocuments($documents, $allowDups = false, $overwritePending = true, $overwriteCommitted = true, $commitWithin = 0)
713
	{
714
		$dupValue = $allowDups ? 'true' : 'false';
715
		$pendingValue = $overwritePending ? 'true' : 'false';
716
		$committedValue = $overwriteCommitted ? 'true' : 'false';
717
 
718
		$commitWithin = (int) $commitWithin;
719
		$commitWithinString = $commitWithin > 0 ? " commitWithin=\"{$commitWithin}\"" : '';
720
 
721
		$rawPost = "<add allowDups=\"{$dupValue}\" overwritePending=\"{$pendingValue}\" overwriteCommitted=\"{$committedValue}\"{$commitWithinString}>";
722
 
723
		foreach ($documents as $document)
724
		{
725
			if ($document instanceof Apache_Solr_Document)
726
			{
727
				$rawPost .= $this->_documentToXmlFragment($document);
728
			}
729
		}
730
 
731
		$rawPost .= '</add>';
732
 
733
		return $this->add($rawPost);
734
	}
735
 
736
	/**
737
	 * Create an XML fragment from a {@link Apache_Solr_Document} instance appropriate for use inside a Solr add call
738
	 *
739
	 * @return string
740
	 */
741
	protected function _documentToXmlFragment(Apache_Solr_Document $document)
742
	{
743
		$xml = '<doc';
744
 
745
		if ($document->getBoost() !== false)
746
		{
747
			$xml .= ' boost="' . $document->getBoost() . '"';
748
		}
749
 
750
		$xml .= '>';
751
 
752
		foreach ($document as $key => $value)
753
		{
754
			$key = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
755
			$fieldBoost = $document->getFieldBoost($key);
756
 
757
			if (is_array($value))
758
			{
759
				foreach ($value as $multivalue)
760
				{
761
					$xml .= '<field name="' . $key . '"';
762
 
763
					if ($fieldBoost !== false)
764
					{
765
						$xml .= ' boost="' . $fieldBoost . '"';
766
 
767
						// only set the boost for the first field in the set
768
						$fieldBoost = false;
769
					}
770
 
771
					$multivalue = htmlspecialchars($multivalue, ENT_NOQUOTES, 'UTF-8');
772
 
773
					$xml .= '>' . $multivalue . '</field>';
774
				}
775
			}
776
			else
777
			{
778
				$xml .= '<field name="' . $key . '"';
779
 
780
				if ($fieldBoost !== false)
781
				{
782
					$xml .= ' boost="' . $fieldBoost . '"';
783
				}
784
 
785
				$value = htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8');
786
 
787
				$xml .= '>' . $value . '</field>';
788
			}
789
		}
790
 
791
		$xml .= '</doc>';
792
 
793
		// replace any control characters to avoid Solr XML parser exception
794
		return $this->_stripCtrlChars($xml);
795
	}
796
 
797
	/**
798
	 * Replace control (non-printable) characters from string that are invalid to Solr's XML parser with a space.
799
	 *
800
	 * @param string $string
801
	 * @return string
802
	 */
803
	protected function _stripCtrlChars($string)
804
	{
805
		// See:  http://w3.org/International/questions/qa-forms-utf-8.html
806
		// Printable utf-8 does not include any of these chars below x7F
807
		return preg_replace('@[\x00-\x08\x0B\x0C\x0E-\x1F]@', ' ', $string);
808
	}
809
 
810
	/**
811
	 * Send a commit command.  Will be synchronous unless both wait parameters are set to false.
812
	 *
813
	 * @param boolean $expungeDeletes Defaults to false, merge segments with deletes away
814
	 * @param boolean $waitFlush Defaults to true,  block until index changes are flushed to disk
815
	 * @param boolean $waitSearcher Defaults to true, block until a new searcher is opened and registered as the main query searcher, making the changes visible
816
	 * @param float $timeout Maximum expected duration (in seconds) of the commit operation on the server (otherwise, will throw a communication exception). Defaults to 1 hour
817
	 * @return Apache_Solr_Response
818
	 *
819
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
820
	 */
821
	public function commit($expungeDeletes = false, $waitFlush = true, $waitSearcher = true, $timeout = 3600)
822
	{
823
		$expungeValue = $expungeDeletes ? 'true' : 'false';
824
		$flushValue = $waitFlush ? 'true' : 'false';
825
		$searcherValue = $waitSearcher ? 'true' : 'false';
826
 
827
		$rawPost = '<commit expungeDeletes="' . $expungeValue . '" waitFlush="' . $flushValue . '" waitSearcher="' . $searcherValue . '" />';
828
 
829
		return $this->_sendRawPost($this->_updateUrl, $rawPost, $timeout);
830
	}
831
 
832
	/**
833
	 * Raw Delete Method. Takes a raw post body and sends it to the update service. Body should be
834
	 * a complete and well formed "delete" xml document
835
	 *
836
	 * @param string $rawPost Expected to be utf-8 encoded xml document
837
	 * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception)
838
	 * @return Apache_Solr_Response
839
	 *
840
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
841
	 */
842
	public function delete($rawPost, $timeout = 3600)
843
	{
844
		return $this->_sendRawPost($this->_updateUrl, $rawPost, $timeout);
845
	}
846
 
847
	/**
848
	 * Create a delete document based on document ID
849
	 *
850
	 * @param string $id Expected to be utf-8 encoded
851
	 * @param boolean $fromPending
852
	 * @param boolean $fromCommitted
853
	 * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception)
854
	 * @return Apache_Solr_Response
855
	 *
856
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
857
	 */
858
	public function deleteById($id, $fromPending = true, $fromCommitted = true, $timeout = 3600)
859
	{
860
		$pendingValue = $fromPending ? 'true' : 'false';
861
		$committedValue = $fromCommitted ? 'true' : 'false';
862
 
863
		//escape special xml characters
864
		$id = htmlspecialchars($id, ENT_NOQUOTES, 'UTF-8');
865
 
866
		$rawPost = '<delete fromPending="' . $pendingValue . '" fromCommitted="' . $committedValue . '"><id>' . $id . '</id></delete>';
867
 
868
		return $this->delete($rawPost, $timeout);
869
	}
870
 
871
	/**
872
	 * Create and post a delete document based on multiple document IDs.
873
	 *
874
	 * @param array $ids Expected to be utf-8 encoded strings
875
	 * @param boolean $fromPending
876
	 * @param boolean $fromCommitted
877
	 * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception)
878
	 * @return Apache_Solr_Response
879
	 *
880
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
881
	 */
882
	public function deleteByMultipleIds($ids, $fromPending = true, $fromCommitted = true, $timeout = 3600)
883
	{
884
		$pendingValue = $fromPending ? 'true' : 'false';
885
		$committedValue = $fromCommitted ? 'true' : 'false';
886
 
887
		$rawPost = '<delete fromPending="' . $pendingValue . '" fromCommitted="' . $committedValue . '">';
888
 
889
		foreach ($ids as $id)
890
		{
891
			//escape special xml characters
892
			$id = htmlspecialchars($id, ENT_NOQUOTES, 'UTF-8');
893
 
894
			$rawPost .= '<id>' . $id . '</id>';
895
		}
896
 
897
		$rawPost .= '</delete>';
898
 
899
		return $this->delete($rawPost, $timeout);
900
	}
901
 
902
	/**
903
	 * Create a delete document based on a query and submit it
904
	 *
905
	 * @param string $rawQuery Expected to be utf-8 encoded
906
	 * @param boolean $fromPending
907
	 * @param boolean $fromCommitted
908
	 * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception)
909
	 * @return Apache_Solr_Response
910
	 *
911
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
912
	 */
913
	public function deleteByQuery($rawQuery, $fromPending = true, $fromCommitted = true, $timeout = 3600)
914
	{
915
		$pendingValue = $fromPending ? 'true' : 'false';
916
		$committedValue = $fromCommitted ? 'true' : 'false';
917
 
918
		// escape special xml characters
919
		$rawQuery = htmlspecialchars($rawQuery, ENT_NOQUOTES, 'UTF-8');
920
 
921
		$rawPost = '<delete fromPending="' . $pendingValue . '" fromCommitted="' . $committedValue . '"><query>' . $rawQuery . '</query></delete>';
922
 
923
		return $this->delete($rawPost, $timeout);
924
	}
925
 
926
	/**
927
	 * Use Solr Cell to extract document contents. See {@link http://wiki.apache.org/solr/ExtractingRequestHandler} for information on how
928
	 * to use Solr Cell and what parameters are available.
929
	 *
930
	 * NOTE: when passing an Apache_Solr_Document instance, field names and boosts will automatically be prepended by "literal." and "boost."
931
	 * as appropriate. Any keys from the $params array will NOT be treated this way. Any mappings from the document will overwrite key / value
932
	 * pairs in the params array if they have the same name (e.g. you pass a "literal.id" key and value in your $params array but you also
933
	 * pass in a document isntance with an "id" field" - the document's value(s) will take precedence).
934
	 *
935
	 * @param string $file Path to file to extract data from
936
	 * @param array $params optional array of key value pairs that will be sent with the post (see Solr Cell documentation)
937
	 * @param Apache_Solr_Document $document optional document that will be used to generate post parameters (literal.* and boost.* params)
938
	 * @param string $mimetype optional mimetype specification (for the file being extracted)
939
	 *
940
	 * @return Apache_Solr_Response
941
	 *
942
	 * @throws Apache_Solr_InvalidArgumentException if $file, $params, or $document are invalid.
943
	 */
944
	public function extract($file, $params = array(), $document = null, $mimetype = 'application/octet-stream')
945
	{
946
		// check if $params is an array (allow null for default empty array)
947
		if (!is_null($params))
948
		{
949
			if (!is_array($params))
950
			{
951
				throw new Apache_Solr_InvalidArgumentException("\$params must be a valid array or null");
952
			}
953
		}
954
		else
955
		{
956
			$params = array();
957
		}
958
 
959
		// if $file is an http request, defer to extractFromUrl instead
960
		if (substr($file, 0, 7) == 'http://' || substr($file, 0, 8) == 'https://')
961
		{
962
			return $this->extractFromUrl($file, $params, $document, $mimetype);
963
		}
964
 
965
		// read the contents of the file
966
		$contents = @file_get_contents($file);
967
 
968
		if ($contents !== false)
969
		{
970
			// add the resource.name parameter if not specified
971
			if (!isset($params['resource.name']))
972
			{
973
				$params['resource.name'] = basename($file);
974
			}
975
 
976
			// delegate the rest to extractFromString
977
			return $this->extractFromString($contents, $params, $document, $mimetype);
978
		}
979
		else
980
		{
981
			throw new Apache_Solr_InvalidArgumentException("File '{$file}' is empty or could not be read");
982
		}
983
	}
984
 
985
	/**
986
	 * Use Solr Cell to extract document contents. See {@link http://wiki.apache.org/solr/ExtractingRequestHandler} for information on how
987
	 * to use Solr Cell and what parameters are available.
988
	 *
989
	 * NOTE: when passing an Apache_Solr_Document instance, field names and boosts will automatically be prepended by "literal." and "boost."
990
	 * as appropriate. Any keys from the $params array will NOT be treated this way. Any mappings from the document will overwrite key / value
991
	 * pairs in the params array if they have the same name (e.g. you pass a "literal.id" key and value in your $params array but you also
992
	 * pass in a document isntance with an "id" field" - the document's value(s) will take precedence).
993
	 *
994
	 * @param string $data Data that will be passed to Solr Cell
995
	 * @param array $params optional array of key value pairs that will be sent with the post (see Solr Cell documentation)
996
	 * @param Apache_Solr_Document $document optional document that will be used to generate post parameters (literal.* and boost.* params)
997
	 * @param string $mimetype optional mimetype specification (for the file being extracted)
998
	 *
999
	 * @return Apache_Solr_Response
1000
	 *
1001
	 * @throws Apache_Solr_InvalidArgumentException if $file, $params, or $document are invalid.
1002
	 *
1003
	 * @todo Should be using multipart/form-data to post parameter values, but I could not get my implementation to work. Needs revisisted.
1004
	 */
1005
	public function extractFromString($data, $params = array(), $document = null, $mimetype = 'application/octet-stream')
1006
	{
1007
		// check if $params is an array (allow null for default empty array)
1008
		if (!is_null($params))
1009
		{
1010
			if (!is_array($params))
1011
			{
1012
				throw new Apache_Solr_InvalidArgumentException("\$params must be a valid array or null");
1013
			}
1014
		}
1015
		else
1016
		{
1017
			$params = array();
1018
		}
1019
 
1020
		// make sure we receive our response in JSON and have proper name list treatment
1021
		$params['wt'] = self::SOLR_WRITER;
1022
		$params['json.nl'] = $this->_namedListTreatment;
1023
 
1024
		// check if $document is an Apache_Solr_Document instance
1025
		if (!is_null($document) && $document instanceof Apache_Solr_Document)
1026
		{
1027
			// iterate document, adding literal.* and boost.* fields to $params as appropriate
1028
			foreach ($document as $field => $fieldValue)
1029
			{
1030
				// check if we need to add a boost.* parameters
1031
				$fieldBoost = $document->getFieldBoost($field);
1032
 
1033
				if ($fieldBoost !== false)
1034
				{
1035
					$params["boost.{$field}"] = $fieldBoost;
1036
				}
1037
 
1038
				// add the literal.* parameter
1039
				$params["literal.{$field}"] = $fieldValue;
1040
			}
1041
		}
1042
 
1043
		// params will be sent to SOLR in the QUERY STRING
1044
		$queryString = $this->_generateQueryString($params);
1045
 
1046
		// the file contents will be sent to SOLR as the POST BODY - we use application/octect-stream as default mimetype
1047
		return $this->_sendRawPost($this->_extractUrl . $this->_queryDelimiter . $queryString, $data, false, $mimetype);
1048
	}
1049
 
1050
	/**
1051
	 * Use Solr Cell to extract document contents. See {@link http://wiki.apache.org/solr/ExtractingRequestHandler} for information on how
1052
	 * to use Solr Cell and what parameters are available.
1053
	 *
1054
	 * NOTE: when passing an Apache_Solr_Document instance, field names and boosts will automatically be prepended by "literal." and "boost."
1055
	 * as appropriate. Any keys from the $params array will NOT be treated this way. Any mappings from the document will overwrite key / value
1056
	 * pairs in the params array if they have the same name (e.g. you pass a "literal.id" key and value in your $params array but you also
1057
	 * pass in a document isntance with an "id" field" - the document's value(s) will take precedence).
1058
	 *
1059
	 * @param string $url URL
1060
	 * @param array $params optional array of key value pairs that will be sent with the post (see Solr Cell documentation)
1061
	 * @param Apache_Solr_Document $document optional document that will be used to generate post parameters (literal.* and boost.* params)
1062
	 * @param string $mimetype optional mimetype specification (for the file being extracted)
1063
	 *
1064
	 * @return Apache_Solr_Response
1065
	 *
1066
	 * @throws Apache_Solr_InvalidArgumentException if $url, $params, or $document are invalid.
1067
	 */
1068
	public function extractFromUrl($url, $params = array(), $document = null, $mimetype = 'application/octet-stream')
1069
	{
1070
		// check if $params is an array (allow null for default empty array)
1071
		if (!is_null($params))
1072
		{
1073
			if (!is_array($params))
1074
			{
1075
				throw new Apache_Solr_InvalidArgumentException("\$params must be a valid array or null");
1076
			}
1077
		}
1078
		else
1079
		{
1080
			$params = array();
1081
		}
1082
 
1083
		$httpTransport = $this->getHttpTransport();
1084
 
1085
		// read the contents of the URL using our configured Http Transport and default timeout
1086
		$httpResponse = $httpTransport->performGetRequest($url);
1087
 
1088
		// check that its a 200 response
1089
		if ($httpResponse->getStatusCode() == 200)
1090
		{
1091
			// add the resource.name parameter if not specified
1092
			if (!isset($params['resource.name']))
1093
			{
1094
				$params['resource.name'] = $url;
1095
			}
1096
 
1097
			// delegate the rest to extractFromString
1098
			return $this->extractFromString($httpResponse->getBody(), $params, $document, $mimetype);
1099
		}
1100
		else
1101
		{
1102
			throw new Apache_Solr_InvalidArgumentException("URL '{$url}' returned non 200 response code");
1103
		}
1104
	}
1105
 
1106
	/**
1107
	 * Send an optimize command.  Will be synchronous unless both wait parameters are set
1108
	 * to false.
1109
	 *
1110
	 * @param boolean $waitFlush
1111
	 * @param boolean $waitSearcher
1112
	 * @param float $timeout Maximum expected duration of the commit operation on the server (otherwise, will throw a communication exception)
1113
	 * @return Apache_Solr_Response
1114
	 *
1115
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
1116
	 */
1117
	public function optimize($waitFlush = true, $waitSearcher = true, $timeout = 3600)
1118
	{
1119
		$flushValue = $waitFlush ? 'true' : 'false';
1120
		$searcherValue = $waitSearcher ? 'true' : 'false';
1121
 
1122
		$rawPost = '<optimize waitFlush="' . $flushValue . '" waitSearcher="' . $searcherValue . '" />';
1123
 
1124
		return $this->_sendRawPost($this->_updateUrl, $rawPost, $timeout);
1125
	}
1126
 
1127
	/**
1128
	 * Simple Search interface
1129
	 *
1130
	 * @param string $query The raw query string
1131
	 * @param int $offset The starting offset for result documents
1132
	 * @param int $limit The maximum number of result documents to return
1133
	 * @param array $params key / value pairs for other query parameters (see Solr documentation), use arrays for parameter keys used more than once (e.g. facet.field)
1134
	 * @param string $method The HTTP method (Apache_Solr_Service::METHOD_GET or Apache_Solr_Service::METHOD::POST)
1135
	 * @return Apache_Solr_Response
1136
	 *
1137
	 * @throws Apache_Solr_HttpTransportException If an error occurs during the service call
1138
	 * @throws Apache_Solr_InvalidArgumentException If an invalid HTTP method is used
1139
	 */
1140
	public function search($query, $offset = 0, $limit = 10, $params = array(), $method = self::METHOD_GET)
1141
	{
1142
		// ensure params is an array
1143
		if (!is_null($params))
1144
		{
1145
			if (!is_array($params))
1146
			{
1147
				// params was specified but was not an array - invalid
1148
				throw new Apache_Solr_InvalidArgumentException("\$params must be a valid array or null");
1149
			}
1150
		}
1151
		else
1152
		{
1153
			$params = array();
1154
		}
1155
 
1156
		// construct our full parameters
1157
 
1158
		// common parameters in this interface
1159
		$params['wt'] = self::SOLR_WRITER;
1160
		$params['json.nl'] = $this->_namedListTreatment;
1161
 
1162
		$params['q'] = $query;
1163
		$params['start'] = $offset;
1164
		$params['rows'] = $limit;
1165
 
1166
		$queryString = $this->_generateQueryString($params);
1167
 
1168
		if ($method == self::METHOD_GET)
1169
		{
1170
			return $this->_sendRawGet($this->_searchUrl . $this->_queryDelimiter . $queryString);
1171
		}
1172
		else if ($method == self::METHOD_POST)
1173
		{
1174
			return $this->_sendRawPost($this->_searchUrl, $queryString, FALSE, 'application/x-www-form-urlencoded; charset=UTF-8');
1175
		}
1176
		else
1177
		{
1178
			throw new Apache_Solr_InvalidArgumentException("Unsupported method '$method', please use the Apache_Solr_Service::METHOD_* constants");
1179
		}
1180
	}
1181
}