1<?php
2
3/**
4 * The tests are relying on FTS index for beer-sample bucket exists with the following mapping for type 'beer':
5 *
6 *    name | text | index | store | include in _all field | include term vectors
7 *    description | text | index | store | include in _all field | include term vectors
8 *    updated | datetime | index | store | include in _all field
9 *    abv | number | index | store | include in _all field
10 *    style | text | index | store | include in _all field | include term vectors
11 *    category | text | index | store | include in _all field | include term vectors
12 */
13class SearchTest extends PHPUnit_Framework_TestCase {
14    public function __construct() {
15        $this->testDsn = getenv('CB_DSN');
16        if ($this->testDsn === FALSE) {
17            $this->testDsn = 'couchbase://localhost/';
18        }
19        $this->authenticator = new \Couchbase\PasswordAuthenticator();
20        $this->authenticator->username(getenv('CB_USER'))->password(getenv('CB_PASSWORD'));
21    }
22
23    protected function setUp() {
24        $this->cluster = new \Couchbase\Cluster($this->testDsn);
25        $this->cluster->authenticate($this->authenticator);
26        $this->bucket = $this->cluster->openBucket('beer-sample');
27    }
28
29    function testSearchWithLimit() {
30        $queryPart = \Couchbase\SearchQuery::matchPhrase("hop beer");
31        $query = new \Couchbase\SearchQuery("beer-search", $queryPart);
32        $query->limit(3);
33
34        $result = $this->bucket->query($query);
35
36        $this->assertNotNull($result);
37        $this->assertNotEmpty($result->hits);
38        $this->assertCount(2, $result->hits);
39        $this->assertEquals(2, $result->metrics['total_hits']);
40
41        foreach ($result->hits as $hit) {
42            $this->assertNotNull($hit->id);
43            $this->assertStringStartsWith("beer-search", $hit->index);
44            $this->assertGreaterThan(0, $hit->score);
45        }
46    }
47
48    function testSearchWithNoHits() {
49        $queryPart = \Couchbase\SearchQuery::matchPhrase("doesnotexistintheindex");
50        $query = new \Couchbase\SearchQuery("beer-search", $queryPart);
51        $query->limit(3);
52
53        $result = $this->bucket->query($query);
54
55        $this->assertNotNull($result);
56        $this->assertEmpty($result->hits);
57        $this->assertEquals(0, $result->metrics['total_hits']);
58    }
59
60    function testSearchWithConsistency() {
61        $cluster = new \Couchbase\Cluster($this->testDsn . '?fetch_mutation_tokens=true');
62        $cluster->authenticate($this->authenticator);
63        $bucket = $cluster->openBucket('beer-sample');
64
65        $id = uniqid('testSearchWithConsistency');
66        $queryPart = \Couchbase\SearchQuery::matchPhrase($id);
67        $query = new \Couchbase\SearchQuery("beer-search", $queryPart);
68        $query->limit(3);
69
70        $result = $bucket->query($query);
71
72        $this->assertNotNull($result);
73        $this->assertEmpty($result->hits);
74        $this->assertEquals(0, $result->metrics['total_hits']);
75
76        $result = $bucket->upsert($id, ["type" => "beer", "name" => $id]);
77        $mutationState = \Couchbase\MutationState::from($result);
78
79        $query->consistentWith($mutationState);
80        $result = $bucket->query($query);
81
82        $this->assertNotNull($result);
83        $this->assertNotEmpty($result->hits);
84        $this->assertEquals(1, $result->metrics['total_hits']);
85        $this->assertEquals($id, $result->hits[0]->id);
86    }
87
88    function testSearchWithFields() {
89        $queryPart = \Couchbase\SearchQuery::matchPhrase("hop beer");
90        $query = new \Couchbase\SearchQuery("beer-search", $queryPart);
91        $query->limit(3)->fields("name");
92
93        $result = $this->bucket->query($query);
94
95        $this->assertNotNull($result);
96        $this->assertNotEmpty($result->hits);
97        $this->assertCount(2, $result->hits);
98        $this->assertEquals(2, $result->metrics['total_hits']);
99
100        foreach ($result->hits as $hit) {
101            $this->assertNotNull($hit->id);
102            $this->assertStringStartsWith("beer-search", $hit->index);
103            $this->assertGreaterThan(0, $hit->score);
104            $this->assertObjectHasAttribute('fields', $hit);
105            $this->assertObjectHasAttribute('name', $hit->fields);
106            $this->assertNotNull($hit->fields->name);
107        }
108    }
109
110    function testSearchWithRanges() {
111        $queryPart = \Couchbase\SearchQuery::numericRange()->field("abv")->min(2.0)->max(3.2);
112        $query = new \Couchbase\SearchQuery("beer-search", $queryPart);
113        $query->fields("abv");
114        $result = $this->bucket->query($query);
115
116        $this->assertNotNull($result);
117        $this->assertNotEmpty($result->hits);
118        $this->assertEquals(count($result->hits), $result->metrics['total_hits']);
119
120        foreach ($result->hits as $hit) {
121            $this->assertNotNull($hit->id);
122            $this->assertStringStartsWith("beer-search", $hit->index);
123            $this->assertGreaterThan(0, $hit->score);
124            $this->assertObjectHasAttribute('fields', $hit);
125            $this->assertObjectHasAttribute('abv', $hit->fields);
126            $this->assertGreaterThanOrEqual(2.0, $hit->fields->abv);
127            $this->assertLessThan(3.2, $hit->fields->abv);
128        }
129
130        $startDate = new DateTime('2010-11-01 10:00:00');
131        $startStr = $startDate->format(DATE_RFC3339);
132        $endInt = mktime(20, 0, 0, 12, 1, 2010);
133        $endDate = DateTime::createFromFormat("U", $endInt);
134        $queryPart = \Couchbase\SearchQuery::conjuncts(
135            \Couchbase\SearchQuery::term("beer")->field("type"),
136            \Couchbase\SearchQuery::dateRange()->field("updated")->start($startStr)->end($endInt)
137        );
138        $query = new \Couchbase\SearchQuery("beer-search", $queryPart);
139        $query->fields("updated", "type");
140        $result = $this->bucket->query($query);
141
142        $this->assertNotNull($result);
143        $this->assertNotEmpty($result->hits);
144        $this->assertEquals(count($result->hits), $result->metrics['total_hits']);
145
146        foreach ($result->hits as $hit) {
147            $this->assertNotNull($hit->id);
148            $this->assertStringStartsWith("beer-search", $hit->index);
149            $this->assertGreaterThan(0, $hit->score);
150            $this->assertObjectHasAttribute('fields', $hit);
151            $this->assertObjectHasAttribute('updated', $hit->fields);
152            $hitDate = new DateTime($hit->fields->updated);
153            $diff = $startDate->diff($hitDate);
154            $this->assertEquals(0, $diff->invert,
155                              "The hit->update date ({$hitDate->format(DATE_RFC3339)}) should go after start date ({$startDate->format(DATE_RFC3339)})");
156            $diff = $endDate->diff($hitDate);
157            $this->assertEquals(1, $diff->invert,
158                              "The hit->update date ({$hitDate->format(DATE_RFC3339)}) should go before or equals to end date ({$startDate->format(DATE_RFC3339)})");
159        }
160    }
161
162    function testCompoundSearchQueries() {
163        $nameQuery = \Couchbase\SearchQuery::match("green")->field("name")->boost(3.4);
164        $descriptionQuery = \Couchbase\SearchQuery::match("hop")->field("description")->fuzziness(1);
165
166        $disjunctionQuery = \Couchbase\SearchQuery::disjuncts($nameQuery, $descriptionQuery);
167        $query = new \Couchbase\SearchQuery("beer-search", $disjunctionQuery);
168        $query->fields("type", "name", "description");
169        $result = $this->bucket->query($query);
170        $this->assertGreaterThan(1000, $result->metrics['total_hits']);
171        $this->assertNotEmpty($result->hits);
172        $this->assertRegexp('/green/i', $result->hits[0]->fields->name);
173        $this->assertNotRegexp('/hop/i', $result->hits[0]->fields->name);
174        $this->assertRegexp('/hop/i', $result->hits[0]->fields->description);
175        $this->assertNotRegexp('/green/i', $result->hits[0]->fields->description);
176
177        $disjunctionQuery->min(2);
178        $query = new \Couchbase\SearchQuery("beer-search", $disjunctionQuery);
179        $query->fields("type", "name", "description");
180        $result = $this->bucket->query($query);
181        $this->assertNotEmpty($result->hits);
182        $this->assertLessThan(10, $result->metrics['total_hits']);
183        $disjunction2Result = $result;
184
185        $conjunctionQuery = \Couchbase\SearchQuery::conjuncts($nameQuery, $descriptionQuery);
186        $query = new \Couchbase\SearchQuery("beer-search", $conjunctionQuery);
187        $query->fields("type", "name", "description");
188        $result = $this->bucket->query($query);
189        $this->assertNotEmpty($result->hits);
190        $this->assertEquals(count($disjunction2Result->hits), count($result->hits));
191        $this->assertEquals($disjunction2Result->hits[0]->fields->name,
192                            $result->hits[0]->fields->name);
193        $this->assertEquals($disjunction2Result->hits[0]->fields->description,
194                            $result->hits[0]->fields->description);
195    }
196
197    function testSearchWithFragments() {
198        $queryPart = \Couchbase\SearchQuery::match("hop beer");
199        $query = new \Couchbase\SearchQuery("beer-search", $queryPart);
200        $query->limit(3)->highlight(\Couchbase\SearchQuery::HIGHLIGHT_HTML, "name");
201
202        $result = $this->bucket->query($query);
203        $this->assertNotEmpty($result->hits);
204
205        foreach ($result->hits as $hit) {
206            $this->assertNotNull($hit->id);
207            $this->assertObjectHasAttribute('fragments', $hit);
208            $this->assertObjectHasAttribute('name', $hit->fragments);
209            foreach ($hit->fragments->name as $fragment) {
210                $this->assertRegexp('/<mark>/', $fragment);
211            }
212        }
213    }
214
215    function testSearchWithFacets() {
216        $queryPart = \Couchbase\SearchQuery::term("beer")->field("type");
217        $query = new \Couchbase\SearchQuery("beer-search", $queryPart);
218        $query
219            ->addFacet("foo", \Couchbase\SearchQuery::termFacet("name", 3))
220            ->addFacet("bar", \Couchbase\SearchQuery::dateRangeFacet("updated", 1)
221                       //->addRange("old", NULL, mktime(0, 0, 0, 1, 1, 2014)))
222                       ->addRange("old", NULL, "2014-01-01T00:00:00"))
223            ->addFacet("baz", \Couchbase\SearchQuery::numericRangeFacet("abv", 2)
224                       ->addRange("strong", 4.9, NULL)
225                       ->addRange("light", NULL, 4.89));
226
227        $result = $this->bucket->query($query);
228        $this->assertNotEmpty($result->hits);
229        $this->assertObjectHasAttribute('facets', $result);
230        $this->assertNotEmpty($result->facets);
231
232        $this->assertNotNull($result->facets['foo']);
233        $this->assertEquals('name', $result->facets['foo']['field']);
234        $this->assertEquals('ale', $result->facets['foo']['terms'][0]['term']);
235        $this->assertGreaterThan(1000, $result->facets['foo']['terms'][0]['count']);
236
237        $this->assertNotNull($result->facets['bar']);
238        $this->assertEquals('updated', $result->facets['bar']['field']);
239        $this->assertEquals('old', $result->facets['bar']['date_ranges'][0]['name']);
240        $this->assertGreaterThan(5000, $result->facets['bar']['date_ranges'][0]['count']);
241
242        $this->assertNotNull($result->facets['baz']);
243        $this->assertEquals('abv', $result->facets['baz']['field']);
244        $this->assertEquals('light', $result->facets['baz']['numeric_ranges'][0]['name']);
245        $this->assertGreaterThan(100, $result->facets['baz']['numeric_ranges'][0]['count']);
246    }
247}
248